对象的[[Prototype]]

js中任何对象内都有一个隐藏属性:[[Prototype]],若访问的属性名在当前对象中没有找到,js会自动到[[Prototype]]所引用的对象中去寻找。[[prototype]]可以通过设置proto属性来间接设置(实际上proto是[[Prototype]]的setter),如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let animal = {
eats: true
walk() {
alert("Animal walk");
}
};

let rabbit = {
jumps: true
};

rabbit.__proto__ = animal;

// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
// walk is taken from the prototype
rabbit.walk(); // Animal walk

函数的原型属性(prototype)

js中的所有函数对象,都自动会创建一个prototype属性,属性值是一个原型对象”prototype”,里面包含一个constructor属性,其属性值设置为函数对象本身。当使用new调用这个函数来创建一个对象的时候,这个对象的[[Prototype]]会指向哪个原型对象,如下例:

1
2
3
4
5
6
7
8
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}

console.log(rabbit.constructor == Rabbit);
// true (from prototype)

avatar

用函数原型实现面向对象

把对象的方法都放在函数原型当中,如下面两个对象:

1
2
3
4
5
6
7
8
9
function Rabbit(name) {
this.name = name;
}

Rabbit.prototype.jump = function() {
alert(`${this.name} jumps!`);
};

let rabbit = new Rabbit("My rabbit");

avatar

1
2
3
4
5
6
7
8
9
function Animal(name) {
this.name = name;
}

Animal.prototype.eat = function() {
alert(`${this.name} eats.`);
};

let animal = new Animal("My animal");

avatar

要实现Rabbit继承Animal,只需要让Rabbit的函数原型的[[Prototype]]指向Animal的函数原型即可。如下代码及图示:

1
2
3
4
5
6
7
8
// setup the inheritance chain
Rabbit.prototype.__proto__ = Animal.prototype; // (*)
// 或者也可以这样
Rabbit.prototype = Object.create(Animal.prototype)

let rabbit = new Rabbit("White Rabbit");
rabbit.eat(); // rabbits can eat too
rabbit.jump();

avatar

创建出rabbit对象实例之后,完整的原型链还包括了Object公共原型,看起来是这样的:

avatar

class关键字

js实现面向对象的基本原理就是之前的那些,js把这些实现集合成语法糖,可以直接使用class关键字像其他语言那样直接定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
class User {

constructor(name) {
this.name = name;
}

sayHi() {
alert(this.name);
}
}

let user = new User("John");
user.sayHi();

上面这段代码,其实就等价于下面这段

1
2
3
4
5
6
7
8
9
10
function User(name) {
this.name = name;
}

User.prototype.sayHi = function() {
alert(this.name);
}

let user = new User("John");
user.sayHi();

一个js中的类,可以写成以下这种形式

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
constructor(...) {
// ...
}
method1(...) {}
method2(...) {}
get something(...) {}
set something(...) {}
static staticMethod(..) {}
// ...
}

Myclass实际上是一个变量,它的值是constructor构造函数,类里的方法如果不加static关键字,就是Myclass的原型内的方法,如果加上static关键字,那么这个函数就是Myclass函数对象内的方法,和Myclass构造函数返回的具体实例无关。

类继承的语法糖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Rabbit extends Animal {
// generated for extending classes without own constructors
constructor(...args) {
super(...args);
// ...
}

hide() {
alert(`${this.name} hides!`);
}

stop() {
super.stop(); // call parent stop
this.hide(); // and then hide
}
}

需要补充的是,当类的原型对象组成继承链的时候,相应的构造函数对象也同时组成了继承链,这样子类也可以调用父类的static方法,如下图所示

avatar

super关键字

super关键字用来访问当前类的父类的方法,它的原理是这样的:

类原型的函数对象里会自动创建一个隐藏的[[HomeObject]]属性,该属性是指向这个原型对象的引用,这样每个类方法都可以通过[[HomeObject]]来访问它真正的原初的this,而不会被人为乱七八糟的调用以及call或者绑定所干扰对真正this值的判断。有了真正的原初的this,就可以通过访问this.[[Prototype]]来获得父类的原型对象了,从而实现了super的效果。

下面用普通的Object写法来展示super的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let animal = {
name: "Animal",
eat() { // [[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};

let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // [[HomeObject]] == rabbit
super.eat(); // animal.eat()
}
};

let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // [[HomeObject]] == longEar
super.eat(); // rabbit.eat()
}
};

longEar.eat(); // Long Ear eats.

需要注意的是,只有写成类方法形式的函数才会生成[[HomeObject]],如果写成”Method : function()”的形式,就不会有[[HomeObject]]。