JavaScript 的原型链
前言
什么是原型
在js中有一句话叫万物皆对象,每个对象都有原型。我们创建函数,如果采用new的方式调用,当然这种调用方式有个名字叫实例化。
// 创建一个函数
function B(name) {
this.name = name;
};
// 实例化
var bb = new B('实例化的b');
console.log(bb.name); // 实例化的b;
如上面的代码,bb是通过B实例化之后得到的对象。在这里B就是一个构造器,他所拥有的名字(this.name)属性会带给bb;这也符合之前杯子的例子,杯子的属性会从构造器中获得。
假如我们想要做出来的bb具有一定的功能,那么就需要在原型上下功夫了。根据上面构造器和原型的关系。我们可以这样做。
// 创建一个函数
function B(name) {
this.name = name;
};
// 在原型上添加一个方法
B.prototype.tan = function() {
alert('弹出框');
}
// 实例化
var bb = new B('实例化的b');
console.log(bb.name); // 实例化的b;
bb.tan(); // alert('弹出框');
在上面的代码中,我们在B的原型上添加了一个tan的方法,在实例化出来的bb也具备了这个方法。这里我们就简单实现了一个类。
用下面一张图,说明一下。实例对象(bb), 原型(prototype), 构造函数(constructor)的关系。
B是我们构造的一个类,这里称为构造函数。他用prototype指向了自己的原型。而他的原型也通过constructor指向了它。
B.prototype.constructor === B; // true;
bb和B没有直接的关联,虽然B是bb的构造函数,这里用虚线表示。bb有一个__ proto__属性,指向了B的prototype
bb.__ proto__ === B.prototype; // true;
bb.__ proto__.constructor = B; // true;
总之
1,每创建一个函数B,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象;
2,原型对象会默认去取得constructor属性,指向构造函数。
3,当调用构造函数创建一个新实例bb后,该实例的内部将包含一个指针__ proto__,指向构造函数的原型对象。
默认原型
我们知道,所有引用对象都默认继承了Object,所有函数的默认原型都是Object的实例。
之前说过构造函数和原型之间具备对应关系,如下:
既然函数的默认原型都是Object的实例,B的原型对象也应该是Object的实例子,也就是说。B的原型的__ proto__应该指向Objct的原型。
Object的原型对象的原型是最底部了,所以不存在原型,指向NULL;
console.log(Object.prototype.__ proto__); // null;
Function对象
我们知道,函数也是对象,任何函数都可以看作是由构造函数Function实例化的对象,所以Function与其原型对象之间也存在如下关系
如果将Foo函数看作实例对象的话,其构造函数就是Function(),原型对象自然就是Function的原型对象;同样Object函数看作实例对象的话,其构造函数就是Function(),原型对象自然也是Function的原型对象。
如果Function的原型对象看作实例对象的话,如前所述所有对象都可看作是Object的实例化对象,所以Function的原型对象的__ proto __指向Object的原型对象。
到这里prototype,__ proto __, constructor三者之间的关系我们就说完了。
实现继承
function Animal() {
this.type = '动物';
}
Animate.prototype.eat = function() {
console.log('吃食物');
}
上面定义了一个动物类,作为父类。
function Cat(name) {
this.name = name || ‘小猫’;
}
定义了一个猫作为子类,这里我们要继承动物类的eat方法和type属性
function Cat(name){
Animal.call(this);
this.name = name || '小猫';
}
在实例化Cat时通过call执行了Animal类, 这样Animal中的this就被修改为当前Cat的this。所有的属性也会加在Cat上。
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化实例方法/属性。而父类的方法仍旧可以赋值给子类。
Cat.prototype = new Super(); 可以实现方法的继承,是因为,根据前面的知识我们知道 new Cat()的__ proto __是指向 Cat的原型的。
(new Cat()).__ proto __ === Cat.prototype; // true
new Cat()所有的方法都是从原型上取到的。
我们通过 Cat.prototype = new Super(); 公式变成了。
(new Cat()).__ proto __ = Cat.prototype = new Super();
所以现在(new Cat()).__ proto __ 指向了 Super的prototype。也就是new Cat的方法是继承自Super.prototype。
Super.prototype又在前一句等于Animal.prototype。所以实现了Cat继承Animal。
这里我们就实现了js属性和方法的继承。不过还在最后一个小问题。
我们知道 prototype 和 constructor 是相互指向的。
Cat.prototype.constructor 应该等于 Cat;
但是随着我们的修改了Cat.prototype = Super.prototype;
现在Cat.prototype.constructor是等于Super的。
所以我们还应该纠正这个问题,一句话搞定。
Cat.prototype.constructor = Cat; // 需要修复下构造函数
以上就是js的原型继承,完整代码如下。
// 创建一个父类
function Animal() {
this.type = '动物';
}
// 给父类添加一个方法
Animate.prototype.eat = function() {
console.log('吃食物');
}
// 创建一个子类
function Cat(name){
// 继承Animal的属性
Animal.call(this);
this.name = name || '小猫';
}
// 继承 Animal 的方法
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
// 修正构造函数
Cat.prototype.constructor = Cat;
好啦,js的继承原理和prototype,proto, constructor之间的关系我们就说完了,ES6底层的实现方式原理基本相同。
学习更多技能
请点击下方公众号