JavaScript中常见的继承:
构造函数的继承 构造函数绑定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Person ( ) { this .species = "人类" ; } Person.prototype.a = 1 Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { Person.apply(this , arguments ) this .name = name; this .age = age; } var ch1 = new Chinese('小明' , 12 )ch1.species ch1.a ch1.eat()
核心代码是 SuperType.call(this)
,创建子类实例的时候调用父类构造函数,所以子类的每个实例都会把父类的属性和方法复制到自身。
不过只能复制父类自身的属性和方法,无法复制父类原型的属性和方法
缺点:
只能继承父类的实例属性和方法,不能继承原型属性和方法
无法实现复用,每个子类都有父类实例函数的副本,影响性能
原型链继承(prototype模式) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function Person ( ) { this .species = "人类" ; this .list = [1 ,2 ,3 ]; } Person.prototype.a = 1 Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { this .name = name; this .age = age; } Chinese.prototype = new Person(); Chinese.prototype.constructor = Chinese var ch1 = new Chinese('小明' , 12 )ch1.species ch1.a ch1.eat() ch1.list ch1.list.push(4 ) ch1.list var ch2 = new Chinese('老周' ,21 );ch2.list
Chinese.prototype = new Person();
相当于完全删除了prototype 对象原先的值,然后赋予一个新值。
那Chinese.prototype.constructor = Chinese
又是什么?
任何一个 prototype
对象都有一个 constructor
属性,指向它的构造函数。如果没有Chinese.prototype.constructor = new Person()
这一行,Chinese.prototype.constructor
会指向 Chinese
的;加了这一行以后,Chinese.prototype.constructor
指向 Person
。
1 2 Chinese.prototype.constructor === Person
这显然会导致继承链的紊乱( ch1
明明是用构造函数 Chinese
生成的),因此我们必须手动纠正。
1 ch1.__proto__.constructor === Person
缺点:
多个实例对原型链上的引用类型的操作会导致原型对象被篡改。
组合继承 组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function Person (name ) { this .species = "人类" ; this .name = name; } Person.prototype.a = 1 Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { Person.apply(this , [name]) this .age = age; } Chinese.prototype = new Person(); Chinese.prototype.constructor = Chinese var ch1 = new Chinese('小明' , 12 )ch1.species ch1.a ch1.eat()
当第一次调用 Person()
:重写 Chinese.prototype
,将 Chinese.prototype
指向 一个 Person
实例,包括两个属性 name
,species
当第二次调用Person()
:给 ch1
附加两个属性name
,color
。再加上 Chinese
构造器添加的 age
属性,ch1
总共有三个属性 name
,color
,age
。
缺点:
使用子类创建实例对象时,其原型中会存在与父类构造器自身相同数量的属性/方法。
直接继承 prototype 该方法是对 原型链继承(prototype 继承) 的改进。
将所有不变的方法/属性都写入 prototype 属性中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function Person (name ) {}Person.prototype.species = 1 Person.prototype.list = [1 ,2 ,3 ]; Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { this .name = name; this .age = age; } Chinese.prototype = Person.prototype; Chinese.prototype.constructor = Chinese var ch1 = new Chinese('小明' , 12 )var ch2 = new Chinese('老周' , 21 )ch1.species ch1.a ch1.eat() ch1.list.push(4 ) ch1.list ch2.list ch1.list === ch2.list
缺点:
是 Chinese.prototype
和 Person.prototype
现在指向了同一个对象,那么任何对 Chinese.prototype
的修改,都会反映到 Person.prototype
。
所以,上面这一段代码其实是有问题的。
1 Person.prototype.constructor === Chinese
而且多个实例对原型链上的引用类型的操作会导致原型对象被篡改。
利用空对象作为中介 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function Person (name ) {}Person.prototype.species = '人类' Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { this .name = name; this .age = age; } function F ( ) {}F.prototype = Person.prototype Chinese.prototype = new F(); Chinese.prototype.constructor = Chinese var ch1 = new Chinese('小明' , 12 )var ch2 = new Chinese('老周' , 21 )ch1.species ch1.a ch1.eat()
常将上面的方法,封装成一个函数,便于使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function Person (name ) {}Person.prototype.species = '人类' ; Person.prototype.list = [1 ,2 ,3 ]; Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { this .name = name; this .age = age; } function extend (Child, Parent ) { var F = function ( ) {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child.constructor; Child.prototype.uber = Parent.prototype; } extend(Chinese, Person); var ch1 = new Chinese('小明' , 12 )var ch2 = new Chinese('老周' , 21 )ch1.species ch1.eat() ch1.list.push(4 ) ch1.list ch2.list ch1.list === ch2.list
extend
函数的最后一行意思是为子对象设一个 uber
属性,这个属性直接指向父对象的 prototype
属性。这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
这个extend
函数,就是YUI库如何实现继承的方法。
而且该函数还有另外一种写法:1 2 3 4 5 6 function extend (Child, Parent ) { var obj = Object .create(Parent.prototype); Child.prototype = obj; Child.prototype.constructor = Child; Child.prototype.uber = Parent.prototype; }
这个方法虽然改进了Chinese.prototype
和 Person.prototype
现在指向了 Chinese
的问题。但是依然有多个实例对原型链上的引用类型的操作会导致原型对象被篡改的问题
缺点:
多个实例对原型链上的引用类型的操作会导致原型对象被篡改。
寄生组合式继承 在上面一种方式的基础上加入构造函数绑定继承来传递参数来实现继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 function Person (name ) { this .name = name; this .list = [11 ,22 ,33 ]; } Person.a = 'a' ; Person.prototype.species = '人类' ; Person.prototype.listOnPrototype = [1 ,2 ,3 ]; Person.prototype.eat = function ( ) { console .log('eat' ); } function Chinese (name, age ) { Person.apply(this , [name]); this .age = age; } function extend (Child, Parent ) { let obj = Object .create(Parent.prototype); Child.prototype = obj; Child.prototype.constructor = Child; Child.prototype.uber = Parent.prototype; } extend(Chinese, Person); var ch1 = new Chinese('小明' , 12 );var ch2 = new Chinese('老周' , 21 );ch1.species ch1.name ch1.age ch1.eat() ch2.species ch2.name ch2.age ch2.eat() ch1.list.push(4 ) ch1.list ch2.list ch1.list === ch2.list
这个例子的高效率体现在它只调用了一次父类构造器,并且因此避免了在子类构造器的原型上创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof
和 isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
混入方式继承多个对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 function Parent1 ( ) { this .a = 'a' ; this .alist = [1 ,2 ,3 ]; } Parent1.prototype.a1 = 'a1' ; Parent1.prototype.a1list = [4 ,5 ,6 ]; function Parent2 ( ) { this .b = 'b' ; this .blist = [10 ,11 ,12 ]; } Parent2.prototype.b1 = 'b1' ; Parent2.prototype.b1list = [13 ,14 ,15 ]; function Child (c ) { Parent1.call(this ); Parent2.call(this ); this .c = c; } Child.prototype = Object .create(Parent1.prototype); Object .assign(Child.prototype, Parent2.prototype);Child.prototype.constructor = Child; Child.prototype.eat = function ( ) { console .log('eat' ) } var ch1 = new Child('c1' );var ch2 = new Child('c2' );ch1.a ch1.alist ch1.a1list ch2.a ch2.alist ch2.a1list ch1.alist.push(4 ) ch1.alist ch2.alist ch1.a1list.push(4 ) ch1.a1list ch2.a1list ch1.a1list === ch2.a1list ch1.b1list === ch2.b1list
是在寄生组合继承的基础上使用 Object.assign
方法在子类构造器的原型上添加属性/方法的方式来继承多个对象。
所有子类实例通过原型链继承并共享子类构造器的原型上的使用 Object.assign
方法添加的属性/方法。
1 2 ch1.__proto__.b1list === Child.prototype.b1list ch1.b1list === ch2.b1list
所有子类实例通过原型链继承并共享父类构造器的原型上的使用 Object.create
方法添加的属性/方法。
1 2 ch1.__proto__.__proto__a1list === Parent1.prototype.a1list ch1.a1list === ch2.a1list
所以
在子类构造器自身的属性/方法不但会被继承而且会被共享。 在父类构造器自身的属性/方法会被继承但是不共享。
浅拷贝继承 把父类的所有不变属性,都放到它的 prototype
对象上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function Person ( ) {}Person.prototype.species = '人类' Person.prototype.list = [1 , 2 , 3 ]; function Chinese (name, age ) { this .name = name; this .age = age; } function copyExtend (Child, Parent ) { let p = Parent.prototype let c = Child.prototype for (const i in p) { c[i] = p[i]; } c.uber = p } copyExtend(Chinese, Person) var ch1 = new Chinese('小明' , 12 );var ch2 = new Chinese('老周' , 21 );ch1.species ch1.list ch1.list === Chinese.prototype.list ch2.list === Chinese.prototype.list ch1.list === ch2.list Chinese.prototype.list === Person.prototype.list ch1.list.push(4 ) ch1.list ch2.list Person.prototype.list Chinese.prototype.constructor === Chinese
原理是将所有父构造器原型上的属性/方法浅复制到自构造器的原型上。
实例通过原型链继承到子类构造器原型上的属性/方法。
缺点:
子类实例对原型链上的引用类型的操作会导致原型对象被篡改,而且不仅是子类构造器上的原型对象会被篡改,父类构造器上的原型对象也会被篡改。
ES6类继承extends 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Rectangle { constructor (height, width) { this .height = height; this .width = width; } get area() { return this .calcArea() } calcArea() { return this .height * this .width; } } const rectangle = new Rectangle(10 , 20 );console .log(rectangle.area);----------------------------------------------------------------- class Square extends Rectangle { constructor (length) { super (length, length); this .name = 'Square' ; } get area() { return this .height * this .width; } } const square = new Square(10 );console .log(square.area);
extends
继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function _inherits (Child, Parent ) { Child.prototype = Object .create(Parent && Parent.prototype, { 'constructor' : { value: 'Child' , enumerable: false , configurable: true , writable: true } }); if (superType) { Object .setPrototypeOf ? Object .setPrototypeOf(subType, superType) : subType.__proto__ = superType; } }
函数声明和类声明的区别:
函数声明会提升,类声明不会。
1 2 3 4 let p = new Rectangle(); class Rectangle {}
首先需要声明你的类,然后才能访问它,否则抛出一个ReferenceError异常。
ES5继承和ES6继承的区别
ES6 extends
继承,主要就是:
将子类构造函数(Child)的原型(proto )指向了父类构造函数(Parent)。即:
1 Child.__proto__ ==== Parent
将子类实例child的原型对象(Child.prototype) 的原型(proto )指向了父类parent的原型对象(Parent.prototype)。
1 Child.prototype.__proto__ ==== Parent.prototype
子类构造函数Child继承了父类构造函数Preant的里的属性。使用super调用的(ES5则用call或者apply调用传参)。
1 2 3 4 function Child (name, age ) { Person.call(this , name); this .age = age; }
寄生组合式继承的原理就是以上的第二和第三点,extends
正是在寄生组合继承的基础上实现了第一点。
非构造函数继承 原型式继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var Person = { species: "人类" , list: [1 ,2 ,3 ] } function object (obj ) { function F ( ) {} F.prototype = obj; return new F(); } var Chinese = object(Person);var Japanese = object(Person);Chinese.Nationality = '中国人' ; Chinese.list Japanese.Nationality = '日本人' ; Japanese.list Chinese.list.push Chinese.list Japanese.list Japanese.list === Chinese.list Chinese.__proto__ === Person Chinese.__proto__ === Japanese.__proto__
object()
对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
缺点:
原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
无法传递参数
另外,ES5中存在Object.create()的方法,能够代替上面的object方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 var Person = { species: "人类" , list: [1 ,2 ,3 ] } var Chinese = Object .create(Person, { 'Nationality' : { value: '中国人' } }); var Japanese = Object .create(Person, { 'Nationality' : { value: '日本人' } }); Chinese.Nationality Japanese.Nationality Chinese.list Japanese.list Chinese.list.push(4 ); Chinese.list Japanese.list Japanese.list === Chinese.list Chinese.__proto__ === Person Chinese.__proto__ === Japanese.__proto__
寄生式继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 var Person = { species: "人类" , list: [1 ,2 ,3 ] } function object (obj ) { function F ( ) {} F.prototype = obj; return new F(); } function createAnother (original ) { var clone = object(original) clone.sayHi = function ( ) { console .log("hi" ); }; return clone; } var Chinese = object(Person);var Japanese = object(Person);Chinese.Nationality = '中国人' ; Japanese.Nationality = '日本人' ; Chinese.list Chinese.sayHi() Japanese.list Japanese.sayHi() Chinese.list.push Chinese.list Japanese.list Japanese.list === Chinese.list Chinese.__proto__ === Person Chinese.__proto__ === Japanese.__proto__
Object.create版:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 var Person = { species: "人类" , list: [1 ,2 ,3 ] } function createAnother (original, propertiesObject ) { var clone = Object .create(original, propertiesObject) clone.sayHi = function ( ) { console .log("hi" ); }; return clone; } var Chinese = createAnother(Person, { 'Nationality' : { value: '中国人' } }); var Japanese = createAnother(Person, { 'Nationality' : { value: '日本人' } }); Chinese.list Chinese.sayHi() Japanese.list Japanese.sayHi() Chinese.list.push Chinese.list Japanese.list Japanese.list === Chinese.list Chinese.__proto__ === Person Chinese.__proto__ === Japanese.__proto__
其实和原型式继承是一样的。只是在其基础上进行封装,为构造函数新增属性和方法。
缺点(同原型式继承):
原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
无法传递参数
浅拷贝 除了使用”prototype链”实现继承以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function extendCopy (p ) { var c = {}; for (var i in p) { c[i] = p[i]; } c.uber = p; return c; } var Person = { species: "人类" , list: [1 ,2 ,3 ] } var Chinese = extendCopy(Person);Chinese.Nationality = '中国人' var Japanese = extendCopy(Person);Japanese.Nationality = '日本人' Chinese.Nationality Japanese.Nationality Chinese.list Japanese.list Chinese.list.push(4 ); Chinese.list Japanese.list Japanese.list === Chinese.list Chinese.__proto__ === Japanese.__proto__
extendCopy()
只是拷贝基本类型的数据,我们把这种拷贝叫做”浅拷贝”。这是早期jQuery实现继承的方式。
深拷贝 深拷贝能够实现真正意义上的数组和对象的拷贝只要递归调用”浅拷贝”即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function deepCopy (p, c ) { var c = c || {}; for (var i in p) { if (typeof p[i] === 'object' ) { c[i] = (p[i].constructor === Array ) ? [] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; } var Person = { species: "人类" , list: [1 ,2 ,3 ] } var Chinese = deepCopy(Person);var Japanese = deepCopy(Person);Chinese.list Japanese.list Chinese.list.push(4 ); Chinese.list Japanese.list
小结 继承对于JS来说就是父类拥有的方法和属性、静态方法等,子类也要拥有。子类中可以利用原型链查找,也可以在子类调用父类,或者从父类拷贝一份到子类等方案。 继承方法有很多,重点在于必须理解这些对象、原型以及构造器的工作方式。
回顾ES6的 extends
。主要原理就是三点:
子类构造函数的proto 指向父类构造器,继承父类的静态方法
子类构造函数的prototype的proto 指向父类构造器的prototype,继承父类的方法。
子类构造器里调用父类构造器,继承父类的属性。
参考 Javascript面向对象编程(二):构造函数的继承 Javascript面向对象编程(三):非构造函数的继承 面试官问:JS的继承 JavaScript常用八种继承方案