class & extend
ES5 & ES6 version
类
工厂模式
1 2 3 4 5 6 7 8 9 10 11 12
| function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson('Nicholas', 29, 'Software Engineer'); var person2 = createPerson('Greg', 27, 'Doctor');
|
方便创建多个相似对象
没有解决对象识别的问题(即如何知道一个对象的类型)
构造函数模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); } } var person1 = new Person('Nicholas', 29, 'Software Engineer'); var person2 = new Person('Greg', 27, 'Doctor');
alert(person1.constructor === Person); alert(person1 instanceof Object); alert(person1 instanceof Person);
|
每个方法都要在每个实例上重新创建一遍
原型模式
1 2 3 4 5 6 7 8 9
| function Person(){} Person.prototype.name = 'Nicholas'; Person.prototype.age = 29; Person.prototype.job = 'Software Engineer'; Person.prototype.sayName = function() { alert(this.name); };
var person1 = new Person();
|
ok:constructor instanceof
属性屏蔽
所有属性都是由实例共享(如果实例这样修改基本属性person1.name还可以,相当于屏蔽了原型里的name属性;函数由于本身就应该是共享的所以也可以;但是如果是引用类型的属性Person.prototype.friends = ['aa', 'bb']
这种,如果person1.friends.push('cc')
这样修改则会修改原型上的friends,导致person2的friends也会被修改)
[常用]组合使用构造函数模式和原型模式
1 2 3 4 5 6 7 8 9 10 11
| function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } }
|
构造函数模式定义实例属性,原型模式定义方法和共享的属性
动态原型模式
1 2 3 4 5 6 7 8 9 10
| function Person(name, age, job) { this.name = name; this.age = age; this.job = job; if (typeof this.sayName !== 'function') { Person.prototype.sayName = function() { alert(this.name); }; } }
|
寄生构造函数模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Person(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { alert(this.name); }; return o; } var person1 = new Person('Nicholas', 29, 'Software Engineer');
alert(person1.constructor === Person); alert(person1 instanceof Object); alert(person1 instanceof Person);
|
工厂模式+new = 构造函数有返回值
不能instanceof,因为这个构造函数有返回值所以new优先使用返回值(Object类型)
稳妥构造函数模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Person(name, age, job) { var o = new Object(); o.sayName = function() { alert(this.name); }; return o; } var person1 = Person('Nicholas', 29, 'Software Engineer');
|
稳妥对象:没有公共属性,而且其方法也不引用this的对象
除了使用sayName方法,没有其他方法能访问name的值
不能instanceof
es6的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class PersonClass { constructor(name) { this.name = name; } sayName() { console.log(this.name); } static create(name) { return new PersonClass(name); } } let person = new PersonClass('Nicholas'); person.sayName();
console.log(person instanceof PersonClass); console.log(person instanceof Object);
let person2 = PersonClass.create('Nicholas');
|
只是语法糖,typeof PersonClass
得到的是'function'
,本质就是[常用]组合使用构造函数模式和原型模式
表达式写法
1 2 3 4 5 6 7 8
| let PersonClass = class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }
|
与我们自己写的普通函数不同的是:
- 类声明不会像函数一样被提升
- 自动严格模式
- 所有方法不可枚举(用Object.defineProperty定义)(通过for…in和in会取到原型链上的属性)
- 不用new调用的话会抛出错误(用new.target判断)
- 在类中修改类名会导致报错,在外部修改就可以(用const实现)
等价的实现
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
| let PersonClass2 = (function() { 'use strict'; const PersonType2 = function(name) { if (typeof new.target === 'undefined') { throw new Error('必须通过new调用构造函数'); } this.name = name; } Object.defineProperty(PersonType2.prototype, 'sayName', { value: function() { if (typeof new.target !== 'undefined') { throw new Error('不可使用new调用该方法'); } console.log(this.name); }, enumerable: false, writable: true, configurable: true }); return PersonType2; })();
|
参考:
- 《JavaScript高级程序设计》第6章 面向对象的程序设计
- 《深入理解es6》第9章 JavaScript中的类
继承
原型链继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; };
function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function() { return this.subproperty; }
var instance = new SubType(); alert(instance.getSuperValue());
|
本质是重写原型对象SubType.prototype,代之以一个新类型的实例
注意:
- 因为使用了new所以
SubType.__proto__=SuperType.prototype
- 如果取
instance.constructor
会通过原型链找到SuperType
instanceof Object / SuperType / SubType都为true
Object / SuperType / SubType .prototype.isPrototypeOf(instance)都为true
缺点:
- 父类中的引用类型属性会被所有子类共享(因为SubType.prototype实际上是SuperType的实例,所以原先父类的实例属性变成了原型属性,就会出现原型模式中的属性共享问题)
- 无法传递参数给SuperType
instance.constructor
指向问题
借用构造函数继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function SuperType() { this.colors = ['red', 'blue', 'green']; }
function SubType() { SuperType.call(this); }
var instance1 = new SubType(); instance1.colors.push('black'); alert(instance1.colors)
var instance2 = new SubType(); alert(instance2.colors)
|
可以在调用SuperType.call时传递参数,解决了原型链继承的属性共享问题
但是只这样写不能继承父类的原型属性/方法,只能继承实例属性和方法
而且同构造函数模式一样,每个子类都有父类实例函数的副本
instance1 instanceof SuperType
会返回false
[常用]组合继承
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
| 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; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { alert(this.age); }
var instance1 = new SubType('Nicholas', 29); instance1.colors.push('black'); alert(instance1.colors); instance1.sayName(); instance1.sayAge();
var instance2 = new SubType('Greg', 27); alert(instance2.colors); instance2.sayName(); instance2.sayAge();
|
会调用两次SuperType(),导致instance
和SubType.prototype
中都有name和colors
原型式继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function object(obj) { function F(){} F.prototype = obj; return new F(); }
var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] };
var anotherPerson = object(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob');
var yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friends.push('Barbie');
alert(person.friends);
|
在一个对象的基础上生成另一个对象,本质是对obj
进行了浅复制
同原型链继承,存在属性共享问题和无法传递参数
等于Object.create()
方法
person.isPrototypeOf(anotherPerson) = true
anotherPerson.__proto__ === person
- instanceof不起效,因为anotherPerson的原型是个对象不是函数,根本没有prototype这个属性,见instanceof和isPrototypeOf()
寄生式继承
1 2 3 4 5 6 7
| function createAnother(original) { var clone = object(original); clone.sayHi = function() { alert('hi'); }; return clone; }
|
是原型式继承的扩展,缺点也是一样的
[常用]寄生组合式继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); 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); }
|
inheritPrototype(SubType, SuperType)这里没有向组合继承一样使用new SuperType(),因为只是想得到SuperType.prototype上的方法,所以只要通过原型式继承搞个浅复制的副本然后再像寄生式继承一样增强一下(修复constructor的指向,和组合继承一样)就可以了
总结起来就是,在[常用]组合使用构造函数模式和原型模式的基础上:
- 构造函数模式对应借用构造函数继承(SuperType.call()),可以解决实例属性继承的问题(不会发生属性共享)
- 原型模式对应原型链继承(new SuperType()),但我们其实只需要SuperType.prototype上的方法所以没必要把整个SuperType实例都搞过来(冗余问题)
- 所以又根据原型式继承和寄生式继承,对原型链继承进行了优化,只对SuperType.prototype进行浅拷贝+增强,最终的SubType.prototype长得就和正常的prototype一样(有我们增强时候加上的constructor,有原型链继承来的方法,而没有冗余的属性)
用Object.create() / Object.setPrototypeOf()的写法
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
| 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; }
SubType.prototype = Object.create(SuperType.prototype, { constructor: { value: SubType, enumrable: true, writaable: true, configurable: true } });
Object.setPrototypeOf(SubType.prototype, SuperType.prototype);
SubType.prototype.sayAge = function() { alert(this.age); }
|
es6的继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Rectangle { constructor(length, width) { this.length = length; this.width = width; } getArea() { return this.length * this.width; } }
class Square extends Rectangle { constructor(length) { super(length, length); } }
var square = new Square(3); console.log(square.getArea()); console.log(square instanceof Square);
|
extends关键字
super方法:
- 在子类的构造函数中一定要调用super(),如果不写构造函数则会自动调用super()并传入所有参数
- super()负责初始化this
super
是指向父类原型的指针(等于Object.getPrototypeOf(this)
),因此发生屏蔽时可以通过super.getArea()访问父类方法
extends还可以用于继承es5风格的构造函数,事实上只要表达式可以被解析为一个函数并具有[[Construct]]属性和原型就可以用extends进行派生
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
行为委托模式
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。js中的[[Prototype]]机制本质上就是行为委托机制。主要通过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
| Foo = { init: function(who) { this.me = who; }, identfy: function() { return 'I am ' + this.me; } };
Bar = Object.create(Foo); Bar.speak = function() { alert('Hello, ' + this.identfy() + '.'); }
var b1 = Object.create(Bar); b1.init('b1'); var b2 = Object.create(Bar); b2.init('b2');
b1.speak(); b2.speak();
Foo.isPrototypeOf(Bar); Object.getPrototypeOf(Bar) === Foo;
Foo.isPrototypeOf(b1); Bar.isPrototypeOf(b1); Object.getPrototypeOf(b1) === Bar;
|
参考:
- 《JavaScript高级程序设计》第6章 面向对象的程序设计
- 《深入理解es6》第9章 JavaScript中的类
- JavaScript常用八种继承方案
- 《你不知道的JavaScript 上卷》第二部分第6章 行为委托