您的位置:首页 > Web前端 > JavaScript

JavaScript-读 You Dont Know JS,原型继承不是继承

2017-05-10 16:25 701 查看
这篇博客是读You Dont Know JS系列书中this & Object Prototypes这本书后总结的第三篇博客,也是最后一篇(第一篇讲this到底是什么,第二篇讲Object到底是什么)。

本篇博客中涉及到原型继承的链式结构、prototype与__proto__(也就是[[prototype]])区别等问题。

继承的本质

在传统的面向对象编程中,大家都习惯抽象一些类,里面封装一些“公共”行为,然后通过该类实例化一些对象,对象上可以定义一些方法来覆盖类上的同名方法,实现每个对象特有的行为。

如果你认为以上这段话没有错误,那你真的需要好好阅读下面的内容。

继承的本质是拷贝。传统的面向对象语言中的父类、子类、实例是基于拷贝的。父类会把自己的方法拷贝到子类中,子类会把自己的方法拷贝到实例中。但是JavaScript中,常用的原型继承,是基于原型链的关联关系,不是拷贝。所以,原型继承不是传统意义上的继承。我们有时候会用mixin来模拟拷贝继承。

原型继承

[[prototype]]

JavaScript中的对象有一个内部属性,在语言规范中称为[[Prototype]],它只是一个其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个非null值。

function Book(){}
let js = new Book();
let java = {};
console.log('js.__proto__', js.__proto__);
console.log('java.__proto__', java.__proto__);




这里的
__proto__
就是对象内部属性
[[prototype]]
,两种方式创建对象
__proto__
的区别在于:使用
new
创建的对象
__proto__
指向创建它的那个函数,字面量创建的对象指向Object。换句话说,new操作符会通过
__proto__
链接两个我们自己创建的对象(函数是内建对象)。

原型链中的prototype与__proto__

当我们访问对象的属性时,默认会寻找该对象上是否存在该属性,存在则返回;不存在则继续寻找
__proto__
属性指向的对象上是否存在该属性;依次寻找,终点是
Object.prototype
。当然也有非默认的情况,就是ES6的Proxy,它可以一次修改一个对象上所有属性的读取和写入操作。

let obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 0;
obj.count++;




至于这个
prototype
,因为取了个和
__proto__
相似的名字而时常被误会。
prototype
仅存在于函数上(普通内建对象是没有这个属性的),当声明一个函数的时候会根据特定规则为这个函数增加一个
prototype
属性,这个属性指向一个新对象,新对象内有一个属性
constructor
,指向这个函数。好复杂的样子,看图:



借用《JavaScript高级程序设计》中的图。

function Book(){}
let js = new Book();
let java = {};

console.log('js.prototype', js.prototype);
console.log('java.prototype', java.prototype);

console.log('Book.prototype', Book.prototype);
console.log('Book.__proto__', Book.__proto__);




只有函数才有
prototype
,因为函数是一类内建对象,所以它也有
__proto__
,也就是
[[prototype]]


我有一张收藏多年的图,仔细分析可以更理解
__proto__
prototype
之间的关系。



原型链上的属性不仅仅覆盖那么简单

大多数开发者认为,如果一个属性已经存在于[[Prototype]]链的高层,那么对它的赋值将总是造成遮蔽。但事实真心没那么简单:

如果一个普通的名为foo的数据访问属性在[[Prototype]]链的高层某处被找到,而且没有被标记为只读(writable:false),那么一个名为foo的新属性就直接添加到myObject上,形成一个 遮蔽属性。

如果一个foo在[[Prototype]]链的高层某处被找到,但是它被标记为 只读(writable:false) ,那么设置既存属性和在myObject上创建遮蔽属性都是 不允许 的。如果代码运行在strict mode下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽。

如果一个foo在[[Prototype]]链的高层某处被找到,而且它是一个setter,那么这个setter总是被调用。没有foo会被添加到(也就是遮蔽在)myObject上,这个foo setter也不会被重定义。

你要小心原型继承

原型继承有两大缺点:

继承关系复杂,如下图

由于prototype的可写性,造成自省复杂(自省指找出类与实例,实例与实例直接的关系)

继承关系

原型继承大家都写过:

function SuperType(){
this.name = 'super type';
}
SuperType.prototype.tellName = function(){
console.log(this.name);
}
SuperType.prototype.newName = 'father';
function SubType(){
this.name = 'sub type';
}

SubType.prototype = Object.create(SuperType.prototype);
// 或者
// SubType.prototype = new SuperType();
let instance = new SubType();


再借《JavaScript高级程序设计》中的一张图:



初学时候想必大家都对这个图困惑不已,到现在也不一定可以不看书的情况下清晰完成此图。

自省

然后,我们为了知道实例所属的类,常常需要进行自省。

如果希望知道类(也就是new操作符调用的那个函数,通常称为构造函数)与实例之间的关系:

// instanceof
console.log(instance instanceof SubType); //true
console.log(instance instanceof SuperType); //true

// isPrototypeOf
console.log(SubType.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true


instanceof回答的问题是:在instance的整个[[Prototype]]链中,有没有出现被那个被Foo.prototype所随便指向的对象?

isPrototypeOf(..)回答的问题是:在instance的整个[[Prototype]]链中,Foo.prototype出现过吗?

如果想知道两个实例对象间的关系:

let superInstance = SubType.prototype;
......
console.log(superInstance.isPrototypeOf(instance)); //true


作者建议你这样理解JS中的继承

继承是为了获得其他对象上的属性,JS中不是用拷贝完成这种“获得”,JS使用原型链来“链接”一些对象(通过[[prototype]])。传统的原型链接方式很复杂,既然仅仅是对象链接,就不要再考虑“类”这个概念,仅考虑如何更简单的进行链接。作者建议我们这样写”继承”:

let SuperType = {
name: 'super type',
tellName: function(){
console.log(this.name);
}
}

let SubType = Object.create(SuperType);
SubType.name = 'sub type';
SubType.sayHi = function(){};

let instance = Object.create(SubType);
instance.name = 'instance';

instance.tellName(); //instance
// 自省更简单明了
console.log(SubType.isPrototypeOf(instance)); //true
console.log(SuperType.isPrototypeOf(instance)); //true


我画了一个关于以上对象关系的图:



是不是简单很多。

mixin模拟拷贝继承

当你真的非常想使用传统的拷贝继承,或者需要多继承这种技术,就可以考虑mixin。很多工具库都有类似mixin或者extend的工具函数,这里简单介绍。

function mixin(source, target){
for(var key in source){
if(!(key in target)){
target[key] = source[key];
}
}
return target
}


这里你可以控制source上的属性会不会覆盖target上的同名属性。

这部分很简单,更多mixin相关内容你可以自己学习,不赘述。

一个悬念

你知道ES6的Class和extend是如何实现的么,不要说是语法糖、JS中没有类,我想问你知道这颗糖里到底包着什么。可以用babel编译一下看看,我会在后面的博客中分析这个问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息