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来模拟拷贝继承。
这里的
至于这个
借用《JavaScript高级程序设计》中的图。
只有函数才有
我有一张收藏多年的图,仔细分析可以更理解
如果一个普通的名为foo的数据访问属性在[[Prototype]]链的高层某处被找到,而且没有被标记为只读(writable:false),那么一个名为foo的新属性就直接添加到myObject上,形成一个 遮蔽属性。
如果一个foo在[[Prototype]]链的高层某处被找到,但是它被标记为 只读(writable:false) ,那么设置既存属性和在myObject上创建遮蔽属性都是 不允许 的。如果代码运行在strict mode下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽。
如果一个foo在[[Prototype]]链的高层某处被找到,而且它是一个setter,那么这个setter总是被调用。没有foo会被添加到(也就是遮蔽在)myObject上,这个foo setter也不会被重定义。
继承关系复杂,如下图
由于prototype的可写性,造成自省复杂(自省指找出类与实例,实例与实例直接的关系)
再借《JavaScript高级程序设计》中的一张图:
初学时候想必大家都对这个图困惑不已,到现在也不一定可以不看书的情况下清晰完成此图。
如果希望知道类(也就是new操作符调用的那个函数,通常称为构造函数)与实例之间的关系:
instanceof回答的问题是:在instance的整个[[Prototype]]链中,有没有出现被那个被Foo.prototype所随便指向的对象?
isPrototypeOf(..)回答的问题是:在instance的整个[[Prototype]]链中,Foo.prototype出现过吗?
如果想知道两个实例对象间的关系:
我画了一个关于以上对象关系的图:
是不是简单很多。
这里你可以控制source上的属性会不会覆盖target上的同名属性。
这部分很简单,更多mixin相关内容你可以自己学习,不赘述。
本篇博客中涉及到原型继承的链式结构、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编译一下看看,我会在后面的博客中分析这个问题。相关文章推荐
- JavaScript-读 You Dont Know JS, Object到底是什么
- JavaScript-读 You Dont Know JS,this到底是什么
- 《You dont know JS》强制类型转换
- javascript prototype的深度探索不是原型继承那么简单第1/3页
- 《You dont know JS》类型篇总结
- javascript中的继承(JS基于原型链的继承),以及JS中的call和apply函数
- 《You dont know JS》值相关总结
- 《You dont know JS》原生函数
- 深入javascript面向对象,js的原型链、继承
- JS学习笔记——JavaScript继承的6种方法(原型链、借用构造函数、组合、原型式、寄生式、寄生组合式)
- So, you think you know JavaScript? (你认为你懂JS吗)
- js面向对象、原型及继承(javaScript高级程序设计第3版)
- node.js javascript理解原型继承
- 【web前端-理解js原型】理解Javascript中的原型对象、原型链和继承
- js(javascript)中的原型继承(经典继承)
- javascript prototype的深度探索不是原型继承那么简单第1/3页
- javascript教程之不完整的继承(js原型链)
- You-Dont-Know-JS
- js原型继承与多态 How to apply virtual function in javascript
- javascript教程之不完整的继承(js原型链)