对于Javascript原型链,是Javascript中很重要的内容,要理解关键有三点:
Javascript中原型链作用是为了实现Javascript中的继承机制。
Javascript中原型链是利用对象关联的方式实现的(不同于一般的类复制)。
Javascript中之所以能使用原型链来实现继承,关键是Javascript对象中属性检索机制。
于是从类基础开始说起。
1. 面向对象
1.1 类基础
在面向对象语言中,经常会使用到类。类是一种设计模式,是某种事物的描述,类一般存在构造函数,构造函数是用于构建实例,实例是类的具体实现,类和实例的关系就类似于蓝图和建筑物的关系一样。
1.2 继承和多态
继承和多态是面向对象的两个重要的特性。对于类似Java的面向对象语言来说,继承是通过创建实例化的过程中,复制父类的属性和方法来实现,并且通过重写父类的方法(实现了多态)。
2. Javascript中对象属性
这里先提前引入原型链,主要是为了描述,具体原型链继承的方式在 3.2 原型链继承 中描述。
2.1 属性获取
Javascript的对象中属性获取的时候,会根据一定的步骤进行取值:
首先判断对象中是否存在该属性,如果存在该属性则返回属性值。
否则访问该对象的原型对象(__proto__
),判断其是否存在该属性,如果存在,则返回该属性值。
否则继续遍历原型对象,直到Object.prototype
如果仍然没有找到该属性则返回undefined
var o1 = {a: 1};
console.log(o1.a); // 1, obj中存在a,直接返回
function F(){};
F.prototype.b = 2;
var o2 = new F;
console.log(o2.hasOwnProperty('b')); // false
console.log(o2.b); // 2, obj的原型对象中存在属性b,返回该属性
var o3 = {};
console.log(o3.b); // undefined , 由于obj.__proto__中并不存在该属性,所以返回undefined
2.2 属性设置
属性设置也会遍历原型链,但是根据属性存在位置以及原型链上属性描述符的不同,可能会存在不同的设置结果:
// 1. 对象中存在该属性 , 直接修改属性值
var o1 = {a: 1};
console.log(o1.__proto__.a); // undefined
console.log(o1.a); // 1
o1.a = 2;
console.log(o1.__proto__.a); // undefined ,原型链上并没有增加该属性
console.log(o1.a); // 2,当前对象属性值被修改
// 2. 对象中不存在该属性,原型链上也不存在该属性,则对象中增加该属性
var o2 = {};
console.log(o2.__proto__.a); // undefined
console.log(o2.hasOwnProperty('a')); // false
o2.a = 1;
console.log(o2.__proto__.a); // undefined ,原型链上并没有增加该属性
console.log(o2.hasOwnProperty('a')); // true
// 3. 对象中不存在该属性,原型链上存在该属性且不为只读,则当前对象增加该属性,并屏蔽原型链属性值
function F() {}
F.prototype.a = 1;
var o3 = new F;
console.log(o3.__proto__.a); // 1
console.log(o3.hasOwnProperty('a')); // false
o3.a = 2;
console.log(o3.__proto__.a); // 1, 原型链上属性没有发生变化
console.log(o3.hasOwnProperty('a')); // true,对象增加属性
console.log(o3.a); // 2,屏蔽原型链上属性
// 4. 对象中不存在该属性,原型链上该属性为只读,则不会在当前对象中增加该属性,且该属性值不变
function F() {}
Object.defineProperty(F.prototype, 'a', {
writable: false,
value: 1
})
var o4 = new F;
console.log(o4.__proto__.a); // 1
console.log(o4.hasOwnProperty('a')); // false
o4.a = 2;
console.log(o4.__proto__.a); // 1, 原型链上属性没有发生变化
console.log(o4.hasOwnProperty('a')); // false,对象中没有增加该属性值
console.log(o4.a); // 1,获取原型链上属性
// 5. 对象中不存在该属性,原型链上存在该属性的setter方法,则会调用该setter方法
function F() {}
Object.defineProperty(F.prototype, 'a', {
set(){
console.log('set a');
}
})
var o5 = new F;
console.log(o5.__proto__.a); // undefined
console.log(o5.hasOwnProperty('a')); // false
o5.a = 2; // 'set a'
console.log(o5.__proto__.a); // undefined, 原型链上属性没有发生变化
console.log(o5.hasOwnProperty('a')); // false,对象中没有增加该属性值
console.log(o5.a); // unedefined,获取原型链上属性值并没有变
3. 继承
3.1 混入
如 1.2 继承和多态 中所说,一般面向对象编程语言的继承都是在对象实例化的时候,采用复制的方式将父类的内容深度复制一份到实例中,由于Javascript中并没有实例化的过程,但是可以通过对象复制的方式来实现继承关系,这样的方式可以叫做混入。
一般的显示混入,在混合对象的过程中,会将目标对象中不存在的属性进行复制添加。
// 显示混入函数
function mixin(target, source) {
for(let key in source){
if(!target.hasOwnProperty(key)){ // 不存在该属性,则添加该属性
target[key] = source[key];
}
}
}
这样就可以将源对象的属性添加到目标对象属性中,类似复制的原理实现继承关系
当然也可以直接创建一个对象包含所有属性,用新对象属性覆盖掉原对象属性并返回。
3.2 原型继承
3.2.1 Function.prototype
和Object.__proto__
Javascript中默认的继承机制并没有使用类似上面的复制机制实现,而是利用Javascript中的对象,通过对象关联的方式进行实现继承,也就是原型继承。
首先Javascript的Function
对象中,默认包含一个不可枚举的prototype
属性,该属性的值为对应的原型对象,其结果为包含一个不可枚举属性constructor
的对象
function F() {}
console.log(F.hasOwnProperty('prototype')); // true
console.log(Object.propertyIsEnumerable(F.prototype)); // false
console.log(F.prototoype); // { constructor: f}
注意:这里我们可以使用new F
的方式创建对象,这种方式类似Java等面向对象语言中的实例化,F
类似构造函数,但是Javascript中不存在类,所以这只能理解为构造函数方法调用。且这里的constructor
并不代表对象的构造关系。
Javascript对象中存在__proto__
属性,指向对象的原型对象
function F() {};
var f = new F();
console.log(f.__proto__ === F.prototype); // true
3.2.2 Javascript原型继承实现
原型继承利用了Object.create()
方法实现
function F() {}
F.prototype.a = 1;
function G() {}
G.prototype = Object.create(F.prototype);
var g = new G;
console.log(g.a); // 1,根据原型链机制获取到原型对象F.protoype中属性'a'的值
console.log(g.__proto__ === G.prototype); // true
console.log(g.__proto__.__proto__ === F.prototype); // true
其中,Object.create()
的实现原理:
function create(o) {
function F(){}
F.prototype = o;
return new F();
}
通过代码我们可以看到,利用Object.create()
关联了对象,使得G
和F
联系了起来,同样通过这个就可以知道为什么Object.create(null)
创建出来的对象对象不在Object.prototype
链上了
3.3 行为委托
对于Function
的对象,利用Function.prototype
原型链,创建了对象的关联,对于两个对象之间,根据Object.create()
的原理,也可以直接创建关联,通过这样的方法,在属性获取不到的时候,当前对象会委托关联对象进行数据获取。
var o1 = {a: 1};
var o2 = Object.create(o1);
console.log(o2.a); //1 , 获取o1对象上的值
因为Javascript中继承本身就是对象之间的关联,所以比起使用原型继承的方式,需要使用new
等看起来像构造函数的方式实现继承关系,利用行为委托更优秀。Javascript继承实现的本质对象关联。
// 原型链实现继承,通常子类含有父类相同方法并进行重写
function F(width, height){
this.width = width;
this.height = height;
}
F.prototype.width = 1
F.prototype.height = 1;
F.prototype.square = function () {
return this.width * this.height
}
function G(width, height){
F.call(this, width, height); // 需要使用这种显示的方式来调用父类构造器实现初始化
}
G.prototype = Object.create(F.prototype);
G.prototype.square = function () {
return 1/2 * this.width * this.height
}
var g = new G(2, 1);
g.square(); // 1;
// 行为委托实现继承,子对象和父对象方法名一般不同
var F = {
init(width, height) {
this.width = width;
this.height = height;
},
rectSquare() {
return this.width * this.height
}
}
var G = Object.create(F);
G.build = function(width, height){
this.init(width, height); // 利用this来实例化对象
};
G.angelSquare = function() {
return 1/2 * this.width * this.height;
}
G.build(2, 1);
G.angelSquare(); // 1;
4. 其他
4.1 ES6中class
ES6中引入了class
关键字和extends
关键字来实现Javascript中的继承,使得看起来更像一般的面向对象语言,但是实际上这里的class只是原型继承的语法糖,本质还是对象的关联,并非类的复制,所以当改变原型对象的内容会影响到对应的对象。
class F {
constructor(name) {
this.name = name;
}
log() {
return 'log:' + this.a
}
}
var f = new F('patrick');
F.prototype.log = function () {
return 'new log:' + this.name
}
console.log(f.log()); // new log: patrick 修改了原型对象,影响了实际对象。
4.2 hasOwnProperty
由于我们所说的Javascript对象中属性获取和设置是需要在原型链上进行查找的,所以使用hasOwnProperty值来判断是否为当前对象属性,可以阻断原型链上的查找,急速性能
4.3 Object.prototype
对于所有的对象,最终原型对象都指向Object.prototype
,而且Object.prototype
的原型对象为null
5. 参考:
《你不知道的Javascript(上篇)》
MDN Inheritance and the prototype chain
网友评论