本文会碰到的知识点:
原型、原型链、函数对象、普通对象、继承
读完本文,可以学到
面向对象也即是OOP,Object Oriented Programming,是计算机的一种编程架构,OOP的基本原则是计算机是由子程序作用的单个或者多个对象组合而成,包含属性和方法的对象是类的实例,但是JavaScript中没有类的概念,而是直接使用对象来实现编程。
特性:
封装:能够将一个实体的信息、功能、响应都封装到一个单独对象中的特性。
由于JavaScript没有public、private、protected这些关键字,但是可以利用变量的作用域来模拟public和private封装特性
var insObject = (function() { var _name = 'hello'; // private return { getName: function() { // public return _name; } } })(); insObject._name; // undefined insObject.getName(); // hello
这里只是实现了一个简单的版本,private比较好的实现方式可以参考深入理解ES6 145页
protected可以利用ES6的Symbol关键字来实现,这里不展开,有兴趣可以讨论
继承:在不改变源程序的基础上进行扩充,原功能得以保存,并且对子程序进行扩展,避免重复代码编写,后面的章节详细描述
多态:允许将子类类型的指针赋值给父类类型的指针;原生JS是弱类型语言,没有多态概念
但是JavaScript也不是不能实现多态的概念,只是如果你之前是学静态语言的同学,理解起来可能有些误差。例子:
比如我们有台电脑mac, 它有一个方法system来获取系统
var mac = { system: function(){ console.log('mac'); } } var getSystem = function() { mac.system(); } getSystem();// mac
某一天我们换成win,为了防止后面又换成mac,我们让getSystem函数有一定的弹性。
var mac = { system: function(){ console.log('mac'); } } var win = { system: function(){ console.log('win'); } } var getSystem = function(type) { if (type == 'mac') { mac.system(); } else if (type == 'win') { win.system(); } } getSystem('mac');// mac getSystem('win');// win
但是很明显这个函数还是有问题,某天我又换成centos呢。。。。我们改写一下getSystem这个函数
var getSystem = function(ins) { if (ins.system instanceOf Function) { ins.system(); } }
这里我们是假设每个系统获取系统的名称都是system,实际开发过程中可能不会这样,这种情况可以用适配器模式来解决。
JavsScript中面向对象的一些概念:
想弄懂面向对象,是不是先看看对象是啥呢?
我们先看一个题目:
[] + {}; // "[object Object]" {} + []; // 0
解释:
在第一行中,{}出现在+操作符的表达式中,因此被翻译为一个实际的值(一个空object)。而[]被强制转换为"“因此{}也会被强制转换为一个string:”[object Object]"。
但在第二行中,{}被翻译为一个独立的{}空代码块儿(它什么也不做)。块儿不需要分号来终结它们,所以这里缺少分号不是一个问题。最终,+ []是一个将[]明确强制转换 为number的表达式,而它的值是0
可能大家会有一个疑问,为什么上面那些属性要加上prototype
在chrome中打印一下var a = {}
数据属性:
特性名称 | 描述 | 默认值 |
---|---|---|
value | 属性的值 | undfined |
writable | 是否可以修改属性的值,true表示可以,false表示不可以 | true |
enumerable | 属性值是否可枚举,true表示可枚举for-in, false表示不可枚举 | true |
configurable | 属性的特性是否可配置,表示能否通过delete删除属性后重新定义属性 | true |
例子:
访问器属性:
特性名称 | 描述 | 默认值 |
---|---|---|
set | 设置属性时调用的函数 | undefined |
get | 写入属性时调用的函数 | undefined |
configurable | 表示能否通过delete删除属性后重新定义属性 | true |
enumerable | 表示能否通过for-in循环返回属性 | true |
访问器属性不能直接定义,一般是通过Object.defineProperty()方法来定义,但是这个方法只支持IE9+, 以前一般用两个非标准方法来实现__defineGetter__()和֖__defineSetter__()
例子:
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function(){ return this._year; }, set: function(newValue){ if (newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; alert(book.edition);
如何检测某个属性是否在对象中?
function Dogs(name) { this.name = name } function BigDogs(size) { this.size = size; } BigDogs.prototype = new Dogs(); var a = new BigDogs('big'); 'size' in a; 'name' in a; 'age' in a;
a.hasOwnProperty('size'); a.hasOwnProperty('name'); a.hasOwnProperty('age');
// es6 var a = Object.create({}, { name: { value: 'hello', enumerable: true, }, age: { value: 11, enumerable: false, } }); // es5 var b = {}; Object.defineProperties(b, { name: { value: 'hello', enumerable: true, }, age: { value: 11, enumerable: false, } }); a.propertyIsEnumerable('name'); a.propertyIsEnumerable('age');
如何枚举对象的属性,并保证不同了浏览器中的行为是一致的?
var a = { supername: 'super hello', superage: 'super name', } var b = {}; Object.defineProperties(b, { name: { value: 'hello', enumerable: true, }, age: { value: 11, enumerable: false, } }); Object.setPrototypeOf(b, a); // 设置b的原型式a 等效的是b.__proto__ = a for(pro in b) { console.log(pro); // name, supername, superage }
var propertyArray = Object.keys(b); // name
var propertyArray = Object.getOwnPropertyNames(b); // name, age
如何判断两个对象是否相等?
我只想说,这个问题说简单很简单,说复杂也挺复杂的传送门
我们看个简单版的
function isEquivalent(a, b) { var aProps = Object.getOwnPropertyNames(a); var bProps = Object.getOwnPropertyNames(b); if (aProps.length != bProps.length){ return false; } for (var i = 0; i < aProps.length; i++) { var propName = aProps[i]; if (a[propName] !== b[propName]) { return false; } } return true;
}
// Outputs: true console.log(isEquivalent({a:1},{a:1})); ```
上面这个函数还有啥问题呢?
有同学可能会有疑问,能不能用Object.is,答案是否定的,Object.is简单来说就是在===的基础上特别处理了NaN,+0,-0,保证了-0和+0不相同,Object.is(NaN, NaN)返回true
Object、Function、Array、Date等js的内置对象都是函数对象
问题:
function a1 () {} const a2 = function () {} const a3 = new Function(); const b1 = {}; const b2 = new Object(); const c1 = []; const c2 = new Array(); const d1 = new a1(); const d2 = new b1();???? const d3 = new c1();???? typeof a1; typeof a2; typeof a3; typeof b1; typeof b2; typeof c1; typeof c2; typeof d1;
上面两行报错的原因,是因为构造函数只能由函数来充当,而b1和c1不是Function的实例,所以不能充当构造器
但是只有Function的实例都是函数对象、其他的实例都是普通对象
我们延伸一下,在看个例子
const e1 = function *(){}; const e2 = new e1(); // Uncaught TypeError: e1 is not a constructor console.log(e1.constructor) // 是有值的。。。 // 规范里面就不能new const e2 = e1();
GeneratorFunction是一个特殊的函数对象
e1.__proto__.__proto__ === Function.prototype
e1的原型实际上是一个生成器函数GeneratorFunction,也就是说
e1.__proto__ === GeneratorFunction.prototype
这行代码有问题么,啊哈哈哈,GeneratorFunction这个关键字主流的JavaScript还木有暴露出来,所以这个大家理解就好啦
虽然不能直接new e1
但是可以new e1.constructor();哈哈哈哈
对象类型 | prototype | proto |
---|---|---|
函数对象 | Yes | Yes |
普通对象 | No | Yes |
只有函数对象具有prototype这个属性
prototype和__proto__都是js在定义一个对象时的预定义属性
prototype被实例的__proto__指向
__proto__指向构造函数的prototype
const a = function(){} const b = {} typeof a // function typeof b // object typeof a.prototype // object typeof a.__proto__ // function typeof b.prototype // undefined typeof b.__proto__ // object a.__proto__ === Function.prototype b.__proto__ === Object.prototype
理解了prototype和__proto__之后,我们来看看之前一直说的为什么JavaScript里面都是对象
const a = {} const b = function () {} const c = [] const d = new Date() a.__proto__ a.__proto__ === Object.prototype b.__proto__ b.__proto__ === Function.prototype c.__proto__ c.__proto__ === Array.prototype d.__proto__ d.__proto__ === Date.prototype Object.prototype.__proto__ //null Function.prototype.__proto__ === Object.prototype Array.prototype.__proto__ === Object.prototype Date.prototype.__proto__ === Object.prototype
延伸一个问题:如何判断一个变量是否是数组?
我们上面已经解释了,这些都是普通对象,普通对象是没有prototype的,他们typeof的值都是object
typeof [] typeof {}
a的原型链是 Array->Object
const a = []; Array.prototype.isPrototypeOf(obj);
const a = []; a instanceof Array
从构造函数入手,但是这个方法和上面的方法都有一问题,不同的框架中创建的数组不会相互共享其prototype属性
const a = []; Object.prototype.toString.call(a); // [Object Array]
ES5 中所有内置对象的[[Class]]属性的值是由规范定义的,但是 ES6 中已经没有了[[Class]]属性,取代它的是[[NativeBrand]]属性,这个大家有兴趣可以自行去查看规范
原理:
问题?这个一定是正确的么?不正确为啥?
提示ES6的Symbol属性
桌面浏览器
移动端浏览器
其实上一节中的prototype和__proto__就是为了构建原型链而存在的,之前也或多或少的说到了原型链这个概念。
看下面的代码:
const Dogs = function(name) { this.name = name; } Dogs.prototype.getName = function() { return this.name } const jingmao = new Dogs('jingmao'); console.log(jingmao); console.log(jingmao.getName());
这段代码的执行过程
1.首先创建了一个构造函数Dogs,传入一个参数name,Dogs.prototype也会自动创建
2.给对象dogs增加了一个方法
3.通过构造函数Dogs实例化了一个对象jingmao
4.输出jingmao的值
可以看到jingmao有两个值name和__proto__,其中__proto__指向Dogs.prototype
5.执行getName方法时,在jingmao中找不到这个方法,就会继续向着原型链继续往上找,也就是通过__proto__,然后就找到了getName方法。
这个过程实际上就是原型继承,实际上JavaScript的原型继承就是利用了__proto__并借助prototype来实现的。
试一试下面 看输出结果是啥?
jingmao.__proto__ === Function.prototype Dogs.prototype 指向什么 Dogs.prototype.__proto__ 指向什么 Dogs.prototype.__proto__.__proto__ 指向什么
上面例子中getName 最终是查找到了,那么如果在原型链中一直没查找到,会怎么样?
例如console.log(jingmao.age)
jingmao 是一个对象可以继续 jingmao.age 不存在,继续 jingmao.__proto__ 是一个对象可以继续 jingmao.__proto__.age 不存在,继续 jingmao.__proto__.__proto__ 是个对象可以继续 jingmao.__proto__.__proto__.age 不存在,继续 jingmao.__proto__.__proto__.__proto__ null,不是对象,到头啦
原型链的概念其实不重要,重要的是要理解,简单来说,原型链就是利用原型让一个引用类型继承另一个应用类型的属性和方法。
最后我们用一张图来结束本节
Array.__proto__ === Function.prototype Object.__proto__ === Function.prototype
还有三点需要注意的:
如果理解了上面这些内容,大家可以自行描述一下,构造函数、原型和实例之间的关系,也可以举例说明
function Dogs (name) { this.name = name; } var jingmao = new Dogs('jingmao');
这个图大家脑子里面自己构想一下?
解释:
构造函数首字母必须大写,用来区分普通函数,内部使用this指针,指向要生成的实例对象,通过new来生成实例对象。
实例就是通过new一个构造函数产生的对象,它有一个属性[[prototype]]指向原型
原型中有一个属性[[constructor]],指向构造函数
这里只是简单介绍一下
Object.hasOwnProperty() 返回一个布尔值,表示某个对象的实例是否含有指定的属性,而且此属性非原型链继承。用来判断属性是来自实例属性还是原型属性。类似还有in操作符,in操作符只要属性存在,不管实在实例中还是原型中,就会返回true。同时使用in和hasOwnProperty就可以判断属性是在原型中还是在实例中
const Dogs = function (age) { this.age = age } Dogs.prototype.getAge = function() { return this.age; } const jingmao = new Dogs(14); jingmao.hasOwnProperty(age);
Object.prototype.isPrototypeOf() 返回一个布尔值,表示指定的对象是否在本对象的原型链中
const Dogs = function (age) { this.age = age } Dogs.prototype.getAge = function() { return this.age; } const jingmao = new Dogs(11); Object.prototype.isPrototypeOf(Dogs); Dogs.prototype.isPrototypeOf(jingmao);
Object.getPrototypeOf 返回该对象的原型
const Dogs = function (age) { this.age = age } Dogs.prototype.getAge = function() { return this.age; } const jingmao = new Dogs(11); jingmao.__proto__ === Object.getPrototypeOf(jingmao)
原型继承就是利用原型链来实现继承
function SuperType() { this.supername = 'super'; } SuperType.prototype.getSuperName= function(){ return this.supername; } function SubType () { this.subname='subname'; } SubType.prototype = new SuperType(); SubType.prototype.getSubName = function (){ return this.subname; } var instance1 = new SubType(); console.log(instance1.getSubName()); console.log(instance1.getSuperName());
需要注意的地方:
实现原型继承的时候不要使用对象字面量创建原型方法,因为这样做,会重写原型链。
function SuperType() { this.supername = 'super'; } SuperType.prototype.getSuperName= function(){ return this.supername; } function SubType () { this.subname='subname'; } SubType.prototype = new SuperType(); SubType.prototype = { getSubName: function (){ return this.subname; } } var instance1 = new SubType(); console.log(instance1.getSubName()); console.log(instance1.getSuperName()); // error
上面使用SubType.prototype = {...}之后,SubType的原型就是Object了,而不是SuperType了。
优点:原型定义的属性和方法可以复用
缺点:
这里的例子来源是JavaScript高级程序设计
在说构造函数继承之前,我们先看一个例子
var a = { name: 'a', }; var name = 'window'; var getName = function(){ console.log(this.name); } getName() // 输出window getName.call(a) // 输出a
执行getName()时,函数体的this指向window,而执行getName.call(a)时,函数体的this指向的是a对象,所以就可以理解啦。接下来我们看如何实现构造函数继承
function SuperType () { this.colors = ['red', 'green']; } function SubType () { // 继承SuperType SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push('blue'); console.log(instance1.colors); // red, green, blue var instance2 = new SubType(); console.log(instance2.colors); // red, green
SuperType.call(this); 这一行代码,实际上意思是在SubType的实例初始化过程中,调用了SuperType的构造函数,因此SubType的每个实例都有colors这个属性
优点:子对象可以传递参数给父对象。
function SuperType(name) { this.name = name; } function SubType(name, age) { name = name || 'hello'; SuperType.call(this, name); this.age = age; } var instance1 = new SubType('scofield', 28); console.log(instance1.name); console.log(instance1.age);
需要注意的地方是在调用父对象的构造函数之后,再给子类型中的定义属性,否则会被重写。
缺点:方法都需要在构造函数中定义,难以做到函数的复用,而且在父对象的原型上定义的方法,对于子类型是不可见的。 ??? 为什么不可见
function SuperType(name) { this.name = name; } SuperType.prototype.getName = function() { return this.name; } SuperType.prototype.prefix = function() { return 'prefix'; } function SubType(name) { SuperType.call(this, name); } var instance1 = new SubType('scofield'); console.log(instance1.name); console.log(instance1.prefix); console.log(instance1.getName()); // Uncaught TypeError: instance1.getName is not a function
组合式继承顾名思义,就是组合两种模式实现JavaScript的继承,借助原型链和构造函数来实现。这样子在原型上定义方法实现了函数的复用,而且能够保证每个实例都有自己的属性。
function SuperType (name) { this.name = name; this.con = []; } SuperType.prototype.getName = function() { return this.name; } function SubType (name, age) { SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.getAge = function() { return this.age; }; var instance1 = new SubType('li', 18); instance1.con.push('test1'); console.log(instance1.con); // test1 console.log(instance1.getAge()); // 18 console.log(instance1.getName()); // li var instance2 = new SubType('hang', 18); console.log(instance2.con); // test1 console.log(instance2.getAge()); // 18 console.log(instance2.getName()); // hang
优点:弥补了原型继承和构造函数的缺点
缺点:父类构造函数调用了两次
原型式继承并没有使用严格意义上的构造函数,借助原型可以基于已有的对象创建新的对象,例如:
function createObject(o) { function newOrient () {}; newOrient.prototype = o; return new newOrient(); }
简单来说createObject函数,对传入的o对象进行的一次浅拷贝。在ES5中新增加了一个方法Object.create(), 它的作用和createObject是一样的,但是只支持IE9+。
var Dogs = { name: 'jingmao', age: 1 } var BigDogs = Object.create(Dogs); BigDogs.name= 'bigjingmao'; BigDogs.size = 'big'; console.log(BigDogs.age);
其中Object.create还支持传入第二个参数,参数与Object.defineProperties()方法的格式相同,并且会覆盖原型上的同名属性。
寄生式继承其实和原型式继承很类似,区别在于,寄生式继承创建的一个函数把所有的事情做完了,例如给新的对象增加属性和方法。
function createAnother(o) { var clone = Object.create(o); clone.size = 'big'; return clone; } var Dogs = { name: 'jingmao', age: 1 } var BigDogs = createAnother(Dogs); console.log(BigDogs.size);
到最后一个了,看看我们之前遗留的问题:
组合继承会调用两次父对象的构造函数,并且父类型的属性存在两组,一组在实例上,一组在SubType的原型上。解决这个问题的方法就是寄生组合式继承。
function inheritPrototype(subType, superType){ // 继承父类的原型 var prototype = Object.create(superType.prototype); // 重写被污染的construct prototype.constructor = subType; // 重写子类的原型 subType.prototype = prototype; }
这个函数就是寄生组合式继承的最简单的实现方式
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType('hello', 18); instance1.__proto__.constructor == SubType
可以看到
但是还有一个问题:
子类如果在原型上添加方法,必须要在继承之后添加,否则会覆盖原来原型上的方法。但是如果这两个类是已存在的类,就不行了
优化一下:
function inheritPrototype(subType, superType){ // 继承父类的原型 var prototype = Object.create(superType.prototype); // 重写被污染的construct prototype.constructor = subType; // 重写子类的原型 subType.prototype = Object.assign(prototype, subType.prototype); }
虽然通过Object.assign来进行copy解决了覆盖原型类型的方法的问题,但是Object.assign只能够拷贝可枚举的方法,而且如果子类本身就继承了一个类,这个办法也不行。
我们知道了ES5中可以通过原型链来实现继承,ES6提供了extends关键字来实现继承,这相对而言更加清晰和方便,首先看看ES6 Class的语法,此处参考http://es6.ruanyifeng.com/#docs/class
1.需要注意的地方。ES6 中类内部定义的所有方法都是不可枚举的
类的属性名称可以使用表达式(区别1)
2.严格模式,ES6 class类和模块内部默认是严格模式
3.construct方法
也就是类的默认方法,如果没有显示的定义,那么会添加一个空的contruct方法
返回值:默认返回实例对象,也就是this,当然也可以显式的返回另外一个对象。
例如:
Class Foo { constructor() { } } new Foo() instanceof Foo // true Class FakeFoo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false
此外类必须通过new 操作符来调用,否则会报错,这个它与普通的构造函数的区别
Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
4.类的实例对象
类的实例的属性,除非显式的定义在this上,否则都是定义在原型上,这里与ES5保持一致
5.类的表达式
与函数一样,类也可以用表达式的方式来定义
const HClass = class Me { getClassName() { return Me.name; } } const hIns = new HClass(); HClass.getClassName(); // Me Me.getClassName(); // error
这里只有HClass是暴露在外部的,Me只有在class的内部使用,如果不需要使用Me,完全可以省略
那么我们知道利用函数表达式可以创建一个立即执行函数,类可以么?
let person = new class { constructor(name) { this.name = name; }, sayName() { console.log(this.name); } }('jack'); persion.sayName()
6.不存在变量提升
这点是和ES5不一样的, ES6并不会把class的声明提到当前作用域的顶部,这与下一节的继承有关系
new Foo() class Foo {}
7.私有属性和私有方法
私有方法ES6并不提供,但是可以变通
const getAge = Symbol('getAge'); export defalut class Person { // 公有方法 getName(name) { return name; }, // 私有方法 [getAge](age) { return age; } }
私有属性ES6也不支持,有提案说加个#表示私有属性
8.this的指向(仔细看看)
类的内部this的指向默认是指向this的实例的,如果单独使用类中的一些包含this的方法,很有可能会报错
class Logger { printName (name = 'there') { this.print(`Hello ${name}`); }, print (text) { console.log(text); } } const logger = new Logger(); const {printName} = logger; printName(); // Uncaught TypeError: Cannot read property 'print' of undefined logger.printName() // Hello there
解决办法:
class Logger { constructor() { this.printName = this.printName.bind(this); } // ... }
9.name属性
10.class中使用get和set函数,可以用来拦截这个属性的存取行为,利用getOwnPropertyDescriptor来查看属性的get和set函数是否有定义
11.如果在类里面在某个方法上加上*,则表示这个方法是Generator函数
12.在类的某个方法前面加上static关键字,表示这个方法是静态方法,这个方法不会被实例继承,只能够通过类来调用,如果这个静态方法中有this,那么this指向的是类,而不是实例
此外静态方法,和非静态方法是可以重名滴
class Foo { static bar () { this.baz(); } static baz () { console.log('hello'); } baz () { console.log('world'); } } Foo.bar() // hello
父类的静态方法可以被子类继承
13.类的静态属性,也就是说是通过类直接访问的属性
Class Foo { p = 1, static: 1, }
上面的两种方法都是错误的,目前静态属性还处于提案中,
Class Foo { p = 1; static p = 1; }
以前我们定义实例属性只能够在construct中定义
14.new.target属性, new.target返回new命令作用的那个构造函数,如果没有通过new来实例对象,那么这个属性的值是undefined
function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('必须使用 new 命令生成实例'); } } var person = new Person('Jack'); // 正确 var notAPerson = Person.call(person, 'Jack'); // 报错
在Class内部调用的时候,new.target返回当前的Class,需要注意一点就是当子类继承父类的时候,返回当前的Class
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); } } class Square extends Rectangle { constructor(length) { super(length, length); } } var obj = new Square(3); // 输出 false
利用这个特点我们可以写出这样的代码
class Rectangle { constructor(length, width) { if(new.Target === Rectangle) { throw new Error('本类不能实例化'); } } } class Square extends Rectangle { constructor(length) { super(length, length); } } var obj = new Square(3); var notobj = new Rectangle();
1.基本概念
Class可以通过extends关键字来实现继承,而ES5中是通过修改原型链来实现继承
子类必须在constructor中调用super方法,否则新建实例的时候会报错,因为子类没有自己的this,是继承与父类,然后进行加工。
class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError
我们回忆一下ES5的继承,实质是首先创建了子类的实例对象,然后把父类的方法添加到子类上。而ES6是先创建父类的实例对象,然后再用子类的构造函数修改this,如果子类没有添加constructor,这个方法会被自动添加
class ColorPoint extends Point { } // 等同于 class ColorPoint extends Point { constructor(...args) { super(...args); } }
还有一点需要注意,在子类的构造函数中,只有调用super后,才可以使用this关键字,否则会报错
2.super关键字,super可以作为函数和对象来使用
class A {} class B extends A { constructor() { super(); // 等价于A.prototype.constructor.call(this) } }
class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m()
ES6 规定,通过super调用父类的方法时,方法内部的this指向当前的子类实例
由于this指向子类的实例,当对super的一个属性复制的时候,赋值会变成子类的属性
3.ES6的__proto__和prototype
我们知道在ES5中,每个对象的__proto__属性,指向对应构造函数的prototype。而ES6里面有两条继承链路,先看一个例子
class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
class A { } class B { } // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B();
再看下一个问题,我们知道ES6是通过extends关键字来实现继承的,那么extends后面的值可以是什么类型呢?我们根据上的两条继承链路就知道,父类应该要有prototype属性,也就是说函数都可以作为父类被继承,此外我们看3中特殊情况
class A extends Object { } A.__proto__ === Object // true A.prototype.__proto__ === Object.prototype // true
class A { } A.__proto__ === Function.prototype // true A.prototype.__proto__ === Object.prototype // true
class A extends null { } A.__proto__ === Function.prototype // true A.prototype.__proto__ === undefined // true
我们知道,以前原生构造函数是无法继承的,原因是因为子类无法获得原生构造函数的内部属性。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性
function MyArray() { Array.apply(this, arguments); } MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true, enumerable: true } }); var colors = new MyArray(); colors[0] = "red"; colors.length // 0 colors.length = 0; colors[0] // "red"
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。因此我们可以自定义原生数据结构的子类,这些是ES5无法做到的
class MyArray extends Array { constructor(...args) { super(...args); } } var arr = new MyArray(); arr[0] = 12; arr.length // 1 arr.length = 0; arr[0] // undefined
6.Mixin的实现,也就是将多个对象合并成一个对象
const a = { a: 'a' }; const b = { b: 'b' }; const c = {...a, ...b}; // {a: 'a', b: 'b'}
上面是一个比较简单的做法,我们看一个完整的实现方式
function mix(...mixins) { class Mix {} for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷贝实例属性 copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== "constructor" && key !== "prototype" && key !== "name" ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } class DistributedEdit extends mix(Loggable, Serializable) { // ... }