达达前端 · 2019年11月17日

一篇JavaScript技术栈带你了解继承和原型链

file

作者 | Jeskson
来源 | 达达前端小酒馆

1

在学习JavaScript中,我们知道它是一种灵活的语言,具有面向对象,函数式风格的编程模式,面向对象具有两点要记住,三大特性,六大原则。

那么是哪些呢?具体的三大特性指的是什么?请记住三大特性:封装(Encapsulation),继承(Inheritance),多态(Polymorphism)。我们常说的封装,继承,多态,三大特点。六大原则指:单一职责原则(SRP),开放封闭原则(OCP),里氏替换原则(LSP),依赖倒置原则(DIP),接口分离原则(ISP),最少知识原则(LKP)。

继承的了解:

继承,如果有很多的类,每个类中的属性或者是方法都不一样,也有些属性或者方法都是相同的,所以如果去定义它们,有时候要重复去定义这些相同的属性或者方法。

这就导致了代码重复性,这就导致了继承的出现,继承就是儿子继承老子的基因一样,让一个类“儿子类”继承它们的“父亲”,这样就可以拥有“父亲”的所有具有相同的属性或者是方法了。这样的类我们称为它叫做“父类”,继承顾名思义就是儿子继承老子,具有老子的属性或者是方法,通过这种的继承方式,让所有的子类都可以访问这些属性或者是方法,而不用每次都在子类中去定义这些属性或者是方法咯,多方便,多快捷,多快好省!

其实JavaScript并不是什么强面向对象语言,因为它的灵活性决定了并不是所有面向对象的特征都适合JavaScript的开发。

我们其实讲到了类,那么类又是怎么理解的呢?

类是什么呢?类是具有属性或者是方法的集合,可以通过类的构造函数创建一个实例的对象。说人话就是,如把人类看做一个类,而我们每一个人就是一个实例的对象,类的实例对象包含两方面:

类的所有非静态(属性或者是方法)
类的所有静态(属性或者是方法)

非静态(属性或者是方法)就是每一个实例的特有的,属于个性。
所有静态(属性或者是方法)就是每一个实例的共性的,属于共性。

说人话就是,个性(非静态)就是每个人的名字都是不相同的,而名字这个属性就是非静态属性;共性(静态)就是每个人都是要吃饭的,而吃饭这个方法就是静态方法。

2

那么在JavaScript中的类是如何实现的呢?

类的实现:

利用函数创建类,利用new关键字就可以生成实例对象;利用构造函数实现非静态(属性或者是方法),利用prototype实现静态(属性或者是方法)。

// 创建函数
function dashucoding() {
 console.log('dashucoding')
}

// 函数赋值
var da = dashucoding() // undefined
// 实例对象
var jeskson = new dashucoding() // {}

其中dashucoding是一个普通函数,也是一个类的构造函数,当调用dashucoding()的时候,它作为一个普通函数会被执行,会输出dashucoding,因没有返回值,就会返回undefined;而当调用new dashucoding()时,会输出dashucoding并且返回一个对象。

我们把dashucoding这个函数来构造对象,所以我们把这个dashucoding看作构造函数。构造对象,构造函数。即通过利用函数,定义构造函数,就相当于定义一个类,通过new关键字,生成一个实例对象。

// 构造函数
function dashucoding(name) {
 this.name = name
}

var da1 = new dashucoding('jeskson');
var da2 = new dashucoding('jeckson');

console.log(da1.name) // jeskson
console.log(da2.name) // jeckson

其中dashucoding构造函数中多个参数,函数体中多this.name=name ,这句中的this指向new关键字返回的实例化对象。

根据构造函数中参数不同,生成的对象中具有的属性name值也是不同的,这里的name是什么呢?看的出来吗?就是这个类的非静态(属性或者方法)。

那么如何利用prototype来实现静态呢?(原型链的知识点)

3

原型对象链,原型链,JavaScript内建的继承方法被称为原型对象链,又称为原型对象继承。有这样一句话,对于一个对象,因为它继承了它的原型对象的属性,所以它可以访问到这些属性,而原型对象也是一个对象,同理,它也可以有自己的原型对象,所以也是可以继承它的原型对象的属性。

what?一脸懵逼,是不是没听懂,我觉得如小白,鬼听得懂。原型继承链概念,对象继承其原型对象,而原型对象继承它的原型对象。这概念说得鬼听得懂哦,what?what?what? 赏你一大嘴巴子,你妈妈买菜必涨价,超级加倍。你爷爷下象棋,必备指指点点。

原型链:prototype?类的prototype是什么?对象的proto是什么?

类中的prototype

被称作原型:在JavaScript中,每当我们定义一个构造函数时,JavaScript引擎中就会自动为这个类添加一个prototype

JavaScript中,当我们使用new来创建一个对象的时候,JavaScript引擎就会自动为这个对象添加一个__proto__属性,并指向其类的prototype

// 构造函数
function dashucoding(name) {
 // 非静态属性
 this.name = name;
}
// 每当我们定义一个构造函数,JavaScript引擎就会自动为这个
// 类中添加一个prototype

console.log(dashucoding.prototype)

var da1 = new dashucoding('jeskson');
var da2 = new dashucoding('jeckson');

// 对象的proto

console.log(da1.__proto__);
console.log(da2.__proto__);

console.log(dashucoding.prototype === da1.__proto__);
// true
console.log(dashucoding.prototype === da2.__proto__);
// true

其中dashucoding.prototype是一个对象,dashucoding类的实例化对象da1,da2都有一个属性__proto__,也是对象。并dashucoding.prototype等于da1或者da2的__proto__

在JavaScript中引用类型的相等意味着它们所指向的都是同一个对象,任何一个实例化对象的__proto__属性都指向其类的prototype。

对象中的__proto__属性:

// 对象赋值
var pro = {
 name: 'jeskson';
}

var person = {
 __proto__: pro
}

console.log(person.name) // jeskson
person.name = 'jeckson'
console.log(person.name) // jeckson

看见没,其中的person并没有定义name属性,而console.log出来的结果是jeskson哦,这是为啥呢?这就是所谓JavaScript中最厉害牛逼的原型链结果。

其实我们回去看代码就知道是有关联的关系的,person中属性__proto__的值为pro,其中的pro指向pro这个对象,pro中的属性具有name:'jeskson'的,也有__proto__属性,值为Object,而Object指向Object,Object的属性也是有__proto__属性,其值为null。

来,接下来,让我们更加懂的说一下情况,当我们访问person.name是,其中的过程是什么样的?

当我们person.name进行访问的时候,可以看到我们并没有写name这个属性,首先,person.name会去找对象中是否有这个name属性,如果没有,它就会去找__proto__属性对象。看到没,在person中是有这个__proto__属性的,别说没有?

没有,你就是没仔细阅读文章,没有,你就是没看文章内容,没有,你就是不适合。

person中__proto__属性对象的值是pro对象,所以person的__proto__指向了pro这个对象,那么就会发现在pro这个对象中具有name这个属性,那么就可以返回其中的值为'jeskson'了。

但是假如我们给person加上了这个name属性的,先看代码我们是不是给它加了name值,这时候我们console.log中的person.name值就不会找__proto__这个属性了,会去先找其中的name属性,值为'jeckson',所以打印返回的'jeckson'。

这里重点说一个:pro的__proto__指向是Object,每个对象中都有__proto__属性,这个属性指向创建出来的对象它们默认是Object类的对象,所以记住对象的属性__proto__自然指向Object.prototype。

好了好了,那么读懂了原型链,就来说上面没说的,运用prototype实现静态(属性或者是方法)。

// 运用prototype实现静态(属性或者方法)
// 构造函数
function dashucoding(name) {
 this.name = name;
}

// 利用prototype实现静态(属性或者是方法)
// 创建了方法
dashucoding.prototype.eat = function() {
 console.log('i eat');
}

// 实例对象
var da1 = new dashucoding('jeskson');
var da2 = new dashucoding('jeckson');

// 给这个人添加方法

da1.eat() // i eat
da2.eat() // i eat

console.log(da1.eat === da2.eat) // true

其中一句:dashucoding.prototype.eat = function(){...},通过dashucoding实例化的对象__proto__都会指向dashucoding.prototype。

原型链的知识点,只要构造函数中没有定义同名的非静态(属性或者是方法),那么每个对象进行访问的时候都是访问其内部找到的eat方法,这样我们就运用原型链,实现了类的静态(属性或者是方法)。

// 构造函数
function dashucoding(name) {
 this.name = name;
 eat() {
  console.log('i eat');
 }
}

// 这就是同名
4

对象的继承,使用对象字面量创建对象时,会隐式的指向Object.prototype为新对象的[[Prototype]],使用Object.create()方法创建对象时,会显示指定新对象的[[Prototype]]Object.create()方法接受两个参数,第一个参数为新对象的[[Prototype]],第二个参数描述了新对象的属性。

又是懵逼了!!!

// 对象字面量形式
var da = {
 name: 'jeskson'
}
// 原型被隐式地设置为Object.prototype形式了,这就懂了

// Object.create()创建,显示指定了Object.prototype
var dada = Object.create(Object.prototype, {
 dashucoding: {
  id: '123',
  code: 'dashucoding',
  value: '前端'
 }
})

请把以上代码记住,牢牢记住。

实现对象的继承:

var da = {
 // 属性
 name: 'jeskson',
 // 方法
 write: function() {
  console.log(this.name);
 }
}

var dada = Object.create(da, {
 name: {value: 'jeckson' }
})

da.write(); // 'jeskson'
dada.write(); // 'jeckson'
console.log(da.hasOwnProperty('write')); // true
console.log(da.isPrototypeOf(dada)) // true
console.log(dada.hasOwnProperty('write') // false
console.log('write' in dada); // true

console.log(dada.__proto__ === da); // true
console.log(dada.__proto__ === Object.prototype) // true

原型链继承:

原型链是JavaScript实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链的基本模式是,让当前构造函数的原型对象等于另一个构造函数的实例。

function A(name, age) {
 this.name = name;
 this.age = age;
 this.family = ["爸爸","妈妈"];
 if(typeof(this.getName) != "function") {
  A.prototype.getName = function() {
   return this.name;
  }
 }
}

function B() {
 this.job = 'IT';
 if(typeof(this.getJob) != "function") {
  B.prototype.getJob = function() {
   return this.job;
  }
 }
}

B.prototype = new A("jeskson", 12);

var da = new B();

// 实例da 的属性name,age是继承自原型B。prototype
console.log(da.name); // jeskson
console.log(da.age); // 12
console.log(da.hasOwnProperty("name")); // false
console.log(da.hasOwnProperty("age")); // false
// 实例da的原型B.prototype被重写,所以da的构造函数指向A
console.log(da.constructor == B);
console.log(da.constructor == A);
// 输出 false, true
// 一个完整的原型链
// da.__proto__ > B.prototype.__proto__ > A.prototype.__proto__ > Object.prototype
console.log(B.prototype.isPrototype(da));
// true
cosole.log(A.prototype.isPrototype(B.prototype));
// true
console.log(Object.protoype.isProtypeOf(A.prototype));
// true

原型链继承:
JavaScript中的对象继承是构造函数基础的基础,几乎所有的函数都有prototype属性,除了通过Function.prototype.bind方法构造出来的函数是个例外,它是可以被替换和修改的。

原型链实现继承,让子类继承父类的静态(属性或者是方法)

// 父类
function Father() {}
Father.prototype.say = function() {
 console.log('father')
}

function Son() {}
var son1 = new Son();
console.log(son1.say); // undefined

// 原型链实现继承的关键代码
Son.prototype = new Father();
var son2 = new Son();
console.log(son2.say) // function(){...}

当我们使用Son.prototype = new Father()后,通过new Sow()生成的对象都会有__proto__属性,这个属性指向Son.prototype。实现了子类继承了父类的静态(属性或者是方法)。

JavaScript中的原型和原型链:

prototype,当我们创建的每一个函数都有一个prototype原型属性,这个属性就是一个指针,指向了一个对象,而这个对象的用途就是可以由特定类型的所有实例共享的属性和方法。使用原型的好处就是可以让所有的对象实例共享原型对象所包含的属性和方法。

function da(){}

da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';

da.prototype.sayName = function() {
 alert(this.name);
}

var person1 = new da();
person1.sayName(); // jeskson

var person2 = new da();
person2.sayName(); // jeskson

alert(person1.sayName() === person2.sayName());
// true

da.prototype指向原型对象,da.prototype.constructor指向da,默认创建一个新函数,它的原型对象只包含constructor属性,da对象的实例的内部属性仅仅指向da.prototype

5

__proto__,所有的对象都具有__proto__属性,隐式原型,指向构造该对象的构造函数的原型对象。

function da() {}
da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';
da.prototype.sayName = function() {
 alert(this.name);
}

// 实例化对象
var person1 = new da();
console.log(da);
// da(){}

console.log(da.prototype);
console.log(da.prototype.__proto__);
console.log(da.prototype.constructor);
console.log(person1);
console.log(person1.__proto__);

原型链:当为对象实例添加一个属性的时候,这个属性会屏蔽掉原型对象中的同名对象。

function da() {}
da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';
da.prototype.sayName=function() {
 alert(this.name);
}

var person1 = new da();
console.log(person1.name); // jeskon

person1.name="jeckson"
console.log(person1.name); // jeckson

person1.name = null
console.log(person1.name); // null

delete person1.name // 删除,实例属性,注意实例属性
console.log(person1.name); // jeskson

构造函数有一个prototype属性,指向的是实例对象的原型对象,原型对象有一个constructor属性,指向的是原型对象对应的构造函数,实例对象有一个__proto__属性,指向的是该实例对象对应的原型对象。

原型方法:

isPrototypeOf()方法用来判断,某个prototype对象和某个实例之间的关系:

alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true

hasOwnProperty()方法,每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承prototype对象的属性。

alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false

in运算符,用来判断某个实例是否含有某个属性,不管是不是本地属性。

alert("name" in cat1); // true
alert("type" in cat1); // true

构造函数,原型,实例之间的关系:

构造函数,是创建对象的一种常用的方式,其他创建对象的方式还包括工厂模式,原型模式,对象字面量等,我们来看一个简单的构造函数。

// 构造函数
function Da(name, age) {
 // 构造函数的命名约定第一个字母使用大写的形式
 this.name = name;
 this.age = age;
}

每一个构造函数都有一个prototype属性,Da.prototype属性其实是一个指针,指向一个对象,该对象拥有一个constructor属性,所以构造函数中的prototype属性指向一个对象。

原型:

无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,在默认情况下,所有原型对象都会自动获得一个constructor()构造函数,这个属性包含一个指向prototype属性所在函数的指针。

一脸懵逼中!!!

构造函数与原型的关系

构造函数中有prototype属性,指向原型对象中constructor,原型对象中有constructor()构造函数,在原型对象中这个constructor指向构造函数中所在指针。

原型:构造函数的prototype属性所指向的对象。(原型对象)

原型这个对象中,有一个constructor属性又指回构造函数本身。

function Da(name,age) {
 this.name = name;
 this.age = age;
}
 
var da = new Da('jeskson', '12');

通过构造函数创建对象的过程叫做实例化,创建出来的对象叫做实例

为原型添加一个方法:

Da.prototype.eat = function() {
 console.log('i eat');
}

在实例中调用该方法:

var da = new Da('jeskson', '12');
da.eat(); // i eat

原型中的属性和方法,在连接到其对应的构造函数的实例上,是可以使用的。

构造函数,实例,原型 的关系

构造函数里有什么?

构造函数里有prototype

原型里有什么?

原型里有constructor,eat()方法

实例里有什么?

实例有属性或者是方法

最好的实例代码:

// 创建 Da1 构造函数
function Da1 () {
 this.name = 'jeskson';
}

// 给Da1构造函数的原型添加方法
Da1.prototype.eat1 = function() {
 console.log('i eat da1');
}

// 创建Da2构造函数
function Da2 () {
 this.name = 'jeckson';
}

// 将Da1的实例对象直接赋值给D2的原型
Da2.prototype = new Da1();

// 给Da2的原型添加一个方法
Da2.prototype.eat2 = function() {
 console.log('i eat da2');
}

// 实例化Da2
var da2 = new Da2();
da2.eat1(); // jeskson
6

原型链继承,函数声明创建函数时,函数的prototype属性被自动设置为一个继承自Object.prototype的对象,该对象有个自己的属性constructor,其值就是函数本身。

// 构造函数
function Da() {}

// JavaScript引擎
Da.prototype = Object.create(Object.prototype, {
 constructor: {
  name: true,
  age: true
 }
});

console.log(Da.prototype.__proto__ === Object.prototype);
// true

创建出来的构造函数都继承自Object.prototype,JavaScript引擎帮你把构造函数的prototype属性设置为一个继承自Object.prototype的对象。

构造函数实现继承,让子类继承了父类的非静态(属性或者是方法)

// 构造函数
function Da(name) {
 this.name = name
}

function Son() {
 Da.apply(this, agruments)
 this.sing = function() {
  console.log(this.name);
 }
}

var obj1 = new Son('jeskson');
var obj2 = new Son('jeckson');

obj1.sing(); // jeskson
obj2.sing(); // jeckson

组合方式实现继承,原型链继承和构造函数基础,实现对父类的静态及其非静态的(属性或者是方法)的继承。

function Father(name) {
 this.name = name
}
Father.prototype.sayName = function() {
 console.log(this.name);
}

function Son() {
 Father.apply(this, arguments)
}
Son.prototype = new Father();
var son1 = new Son('jeskson');
var son2 = new Son('jeckson');

son1.sayName(); // jeskson
son2.sayName(); // jeckson

寄生组合方式实现继承:Super函数,让Father的原型寄生在Super的原型上,让Son去继承Super,然后把这个过程放到一个闭包内。

// 构造函数
function Father(name) {
 this.name = name
}

Father.prototype.sayName = function() {
 console.log(this.name);
}

function Son() {
 Father.apply(this,arguments)
}

(function() {
 function Super(){}
 Super.prototype = Father.prototype
 Son.prototype = new Super()
}())
var son1 = new Son('jeskson');

子类型构造函数的内部调用父类构造函数

function getArea() {
    return this.length * this.width
}

/* 四边形 */
function Rectangle(length, width) {
    this.length = length
    this.width = width
}

/* 获取面积 */
Rectangle.prototype.getArea = getArea

/* 获取尺寸信息 */
Rectangle.prototype.getSize = function() {
    console.log(`Rectangle: ${ this.length }x${ this.width },面积: ${ this.getArea() }`)
}

/* 正方形 */
function Square(size) {
    Rectangle.call(this, size, size)
    
    this.getArea = getArea
    
    this.getSize = function() {
        console.log(`Square: ${ this.length }x${ this.width },面积: ${ this.getArea() }`)
    }
}

var rect = new Rectangle(5, 10)
var squa = new Square(6)

rect.getSize()       // Rectangle: 5x10,面积: 50
squa.getSize()       // Square: 6x6,面积: 36
7

面向对象,创建对象,使用构造函数创建

var obj = new Object();

字面量创建:

var obj = {};

工厂模式:

var p1 = new Object();
p1.name = 'da';
p1.age = '12'
p1.showName = function() {
 return this.name
}

var p2 = new Object();
p2.name = 'da2';
p2.age = '23',
p2.showName = function() {
 return this.name
}

采用工厂模式,抽象创建对象的过程,封装相同的属性或者是方法

function createF(name, age) {
 var obj = new Object();
 obj.name = name;
 obj.age = age;
 obj.showName = function() {
  return this.name;
 };
 return obj;
}

var p1 = createF('jeskson',12);
var p2 = createF('jeckson',23);

构造模式:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showName = function() {
        console.log(this.name);
    }
}

var p1 = new Person('张三', '1');
var p2 = new Person('李四', '2');

什么是原型链:

在JavaScript中继承的主要方法就是通过原型链,主要是一个原型对象等于另一个类型的实例,由于实例内部含有一个指向构造函数的指针,相当于重写了该原型对象,此时该原型对象包含了一个指向另一个原型的指针。原型链的底层是:Object.prototype.__proto__,值为null。

JavaScript只有一种结构就是对象,每个实例对象都有一个私有的属性为__proto__,它的指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象__proto__,层层向上直到一个对象的原型对象为null。

基于原型链的继承,JavaScript对象有一个指向一个原型对象的链,Object.prototype属性表示Object的原型对象。

let f = function() {
 this.a = 1;
 this.b = 2;
}

// 
function f() {
 this.a = 1;
 this.b = 2;
}
//
let o = new f(); // {a:1,b:2}
f.prototype.b=3;
f.prototype.c=4;

当继承的函数被调用时,this指向的是当前继承的对象,而不是继承的函数所在的原型对象。

var o = {
 a: 2,
 m: function() {
  return this.a+1;
 }
};

console.log(o.m()); // 3
当调用o.m时,'this'指向了o

var p = Object.create(o);
// p是一个继承自o的对象

p.a = 4;
console.log(p.m());
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

使用Object.create创建对象

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, 因为d没有继承Object.prototype

关于目前文章内容即涉及前端,PHP知识点,如果有兴趣即可关注,很荣幸,能被您发现,真是慧眼识英!也感谢您的关注,在未来的日子里,希望能够一直默默的支持我,我也会努力写出更多优秀的作品。我们一起成长,从零基础学编程,将 Web前端领域、数据结构与算法、网络原理等通俗易懂的呈现给小伙伴。分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯。


意见反馈:
若本号内容有做得不到位的地方(比如:涉及版权或其他问题),请及时联系我们进行整改即可,会在第一时间进行处理。


感谢阅读,原创不易,喜欢就点个赞吧,这是我写作最大的动力。

欢迎关注达达的简书!

这是一个有质量,有态度的博客

博客

推荐阅读
关注数
2
文章数
124
前端资源下载群:通告:前端开发交流群:711613774 文章 每天更新(点个赞,证明你还爱我哦,亲)友链大礼包:[链接]
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息