# 关键字
理解三个关键字:
function
: JS 世界里 Class 的定义用function
,function
里面的内容就是构造函数的内容this
: 代表调用这个函数的对象prototype
: 用它来定义成员函数,比较规范和保险
简单例子:
// 定义 Circle 类,拥有成员变量 r,常量 PI 和计算面积的成员函数 area()
function Circle(radius) {
this.r = radius;
}
Circle.PI = 3.1415926;
Circle.prototype.area = function() {
return Circle.PI * this.r * this.r;
}
// 使用 Circle 类
var c = new Circle(1.0);
console.log(c);
另外成员函数也可以写成这样:
function compute_area() {
return Circle.PI * this.r * this.r;
}
Circle.prototype.area = compute_area;
# 创建对象
# 1. 工厂模式
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
}
return o;
}
var person1 = createPerson('jimco', 24, 'Front-end Engineer');
var person2 = crestePerson('Lucy', 25, 'Dcotor');
# 2. 构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
var person1 = new Person('jimco', 24, 'Front-end Engineer');
var person2 = new Person('Lucy', 25, 'Dcotor');
创建 Person 的新实例,必须使用 new
操作符,以这种方式调用构造函数实际上会经历以下 4 个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向这个新对象)
- 执行构造函数中的代码(为这个对象添加属性)
- 返回新对象
# 3. 原型模式
function Person() {}
Person.prototype.name = 'jimco';
Person.prototype.age = '24';
Person.prototype.job = 'Front-end Engineer';
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName); // true
更简单的原型语法:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'jimco',
age: 24,
job: 'Front-end Engineer',
sayName: function() {
console.log(this.name);
}
}
# 4. 组合使用构造函数模式和原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby', 'Court'];
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
}
var person1 = new Person('jimco', 24, 'Front-end Engineer');
var person2 = new Person('Lucy', 25, 'Dcotor');
person1.friends.push('Van');
console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
# 继承
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor
),而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,如此层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。
# 1. 原型链继承
- 原理:将父类实例作为子类原型
- 优点:方法复用
- 缺点:
- 父类上引用类型的属性,会被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperVal = function() {
return this.property;
}
function SubType = {
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // 若不重写则指向 SuperType
SubType.prototype.getSubVal = function() {
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperVal()); // true
包含引用类型值的属性会被所有实例共享:
function SuperType() {
this.colors = ['red', 'green', 'blue'];
}
function SubType() {}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue', 'black']
# 2. 构造函数继承
- 原理:通过在子类构造函数的内部调用父类的构造函数,从而实现继承
- 优点:实例之间独立
- 缺点:
- 父类方法不能复用(由于方法在父构造函数中定义,每次创建子类实例都要创建一遍方法,导致方法不能复用)
- 子类只能拿到父类的属性和方法,无法拿到父类原型的属性
function SuperType() {
this.colors = ['red', 'green', 'blue'];
this.getColors = function() {
console.log(this.colors);
}
}
function SubType() {
// 继承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue']
# 3. 组合继承
- 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用
- 优点:
- 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数
- 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用
- 不共享父类的引用属性
- 缺点:无论在什么情况下,都会调用 2 次父类构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数内部
- 第一次
SuperType.call(this);
从父类拷贝一份父类实例属性,作为子类的实例属性 - 第二次
SubType.prototype = new SuperType()
创建父类实例作为子类原型,此时这个父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉,所以多余
- 第一次
注意:使用「组合继承」时要记得修复 SubType.prototype.constructor
指向
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(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() {
console.log(this.age);
}
var instance1 = new SubType('jimco', 24);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
instance1.sayName(); // 'Jimco'
instance1.sayAge(); // 24
var instance2 = new SubType('Lucy', 25);
console.log(instance2.colors); // ['red', 'green', 'blue']
instance2.sayName(); // 'Lucy'
instance2.sayAge(); // 25
# 4. 组合继承2
- 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类原型作为子类原型,实现函数复用
- 优点:
- 只调用一次父类构造函数
- 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数
- 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用
- 缺点:在修正子类构造函数的指时,父类实例的构造函数指向同时也发生变化(这是我们不希望的)
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name); // 继承属性
this.age = age;
}
SubType.prototype = SuperType.prototype; // 继承方法
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
const p1 = new SuperType('jim);
console.log(p1.constructor); // SubType
# 5. 原型式继承
- 原理:原型式继承的
object
方法本质上是对参数对象的一个浅复制 - 优点:父类方法可以复用
- 缺点:
- 父类的引用属性会被所有子类实例共享
- 子类构建实例时不能向父类传递参数
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
var person = {
name: 'jimco',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = object(person);
person1.name = 'Greg';
person1.friends.push('Bob');
var person2 = object(person);
person2.name = 'Linda';
person2.friends.push('Barbie');
console.log(person2.friends); // ['Shelby', 'Court', 'Van', 'Bob', 'Barbie']
object()
方法与 ECMAScript 5 新增的 Object.create()
方法类似。
在没有必要兴师动众地创建构造函数的,而只想让一个对象与另一个对象保持类似的情况下,原型式继承完全是可以胜任的。不过,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
# 6. 寄生式继承
- 原理:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力
- 优点:-
- 缺点:与构造函数模式类似,方法无法复用
function createAnother(original) {
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
console.log('hi');
}
return clone; // 返回这个对象
}
var person = {
name: 'jimco',
friend: ['Shelby', 'Court', 'Van']
}
var person1 = createAnother(person);
person1.sayHi(); // 'hi'
# 7. 寄生组合式继承
- 原理:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类原型的拷贝作为子类原型,实现函数复用(本质是对组合继承2缺点的优化改进)
- 优点:最理想的继承范式
- 缺点:-
// 同 Object.create
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
这个例子的高效率体现在它只调用了一次 SuperType
构造函数,并且避免了在 SuperType.prototype
上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此还能够正常使用 instanceof
和 isPrototypeOf()
。
# 8. ES6 继承
通过 Class 的 extends
+ super
实现继承。
ES6 规定,子类必须在 constructor()
方法中调用 super()
,否则就会报错。这是因为子类自己的 this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用 super()
方法,子类就得不到自己的 this
对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
构造函数没有调用 super()
,导致新建实例时报错。
为什么子类的构造函数,一定要调用 super()
?原因在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即「实例在前,继承在后」。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即「继承在前,实例在后」。这就是为什么 ES6 的继承必须先调用 super()
方法,因为这一步会生成一个继承父类的 this
对象,没有这一步就无法继承父类。
在子类的构造函数中,只有调用 super()
之后,才可以使用 this
关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有 super()
方法才能让子类实例继承父类。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
上面代码中,子类的 constructor()
方法没有调用 super()
之前,就使用 this
关键字,结果报错,而放在 super()
之后就是正确的。
如果子类没有定义constructor()
方法,这个方法会默认添加,并且里面会调用 super()
。也就是说,不管有没有显式定义,任何一个子类都有constructor()
方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
除了私有属性,父类的所有属性和方法,都会被子类继承,其中包括静态方法。
class Foo {
#p = 1;
#m() {
console.log('hello');
};
static hello() {
console.log('hello world');
}
}
class Bar extends Foo {
constructor() {
super();
console.log(this.#p); // 报错
this.#m(); // 报错
}
}