JavaScript 原型链机制

前言

刚开始接触 JavaScript,我十分不愿意去理解原型链,一是觉得很麻烦和很复杂,二是实际应用场景较少,所以之前都只会基本使用,而工作一段时间过后发现偶尔会接触到,就在此总结一下。

继承

在写 Java 的时候,我们都基本上用类的概念,而到 JavaScript,感觉像是如果我需要什么对象我就const person = {...},感觉不是那么严谨,比方说我有跟 Java 很类似的需求,需要一个Person,然后在这个基础上扩展Student,这感觉就没办法做到了,但其实 JavaScript 在没有class概念之前都是使用原型链:
JavaScript 讲什么严谨,找你的 TypesCript 去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person() {}

Person.prototype.run = function() {
console.log("person running...");
};

function Student() {
this.name = "foo";
}

Student.prototype = new Person();
Student.prototype.read = function() {
console.log("student reading...");
};

const foo = new Student();
foo.run(); // person running...
foo.read(); // student reading...
console.log(foo.name); // foo

这样我们就可以扩展的Person

prototype

上面例子讲了如何通过继承扩展,但是没有明白什么是prototypeprototype实际上就是原型的意思, 在Student.run()的时候,就会去找Student下寻找这个方法,如果没有,就往上面找,就是Student.prototype指向Person的时候,已经绑定了关系

Student.prototype.run() -> Person.prototype.run()

当调用属性或者方法的时候,就会一直找下去,比如我调用foo.toString(),由于StudentPerson都没有这个方法,所以他会继续找下去,就是Object,此时寻找过程就是

Student.prototype.toString() -> Person.prototype.toString() -> Object.prototype.toString()

到了Object后面,就没了,只剩下null

验证

怎么验证一下呢?验证方法有几种

__proto__

所有实例里面都会有一个[[prototype]]指向原型,我们无法直接访问,但是可以通过Object.prototype.__proto__来访问(实际上他是一个 getter 和 setter function)

1
2
3
console.log(foo.__proto__ === Student.prototype); // true
console.log(foo.__proto__.__proto__ === Person.prototype); // true
console.log(foo.__proto__.__proto__.__proto__ === Object.prototype); //true

Object.getPrototypeOf

由于__proto__可以获取也可以设置,比较危险而且代码不太雅观,已经不建议使用,存在只为了兼容性,现在在访问的时候已经建议使用Object.getPrototypeOf()来实现。

1
2
3
4
Object.getPrototypeOf(foo) === Student.prototype; // true
Object.getPrototypeOf(Object.getPrototypeOf(foo)) === Person.prototype; // true
Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(foo))) ===
Object.prototype; // true

instanceof

也可以通过instanceof来验证,

1
2
3
console.log(foo instanceof Student); // true
console.log(foo instanceof Person); // true
console.log(foo instanceof Object); // true

Prototype.isPrototypeOf

或者通过isPrototypeOf来检查

1
2
3
console.log(Student.prototype.isPrototypeOf(foo)); // true
console.log(Person.prototype.isPrototypeOf(foo)); // true
console.log(Object.prototype.isPrototypeOf(foo)); // true

判断属性

我们知道如何验证原型链正确与否,那么如何区分实例上的属性是实例的还是原型的呢?

Object.hasOwnProperty

Object.hasOwnProperty()方法可以知道指定属性是否是实例上的,因为实际上constructor什么都没做,foo在实例的角度看就是空对象{}

1
2
3
console.log(foo.hasOwnProperty("run")); // false
console.log(foo.hasOwnProperty("read")); // false
console.log(foo.hasOwnProperty("name")); // true

in

in操作符可以检查所有属性,如果配合for使用可以循环所有属性

1
2
3
console.log("run" in foo); // true
console.log("read" in foo); // true
console.log("name" in foo); // true

如果我们只需要检查是否原型,可以配合hasOwnProperty使用

1
2
3
4
5
6
7
function hasPrototypeProperty(obj, name) {
return !obj.hasOwnProperty(name) && name in obj;
}

console.log(hasPrototypeProperty(foo, "run")); // true
console.log(hasPrototypeProperty(foo, "read")); // true
console.log(hasPrototypeProperty(foo, "name")); // false

Object.getOwnPropertyNames

Object.getOwnPropertyNames() 方法可以获取所有实例属性

1
console.log(Object.getOwnPropertyNames(foo)); // [ 'name' ]

Object.keys

Object.keys()Object.getOwnPropertyNames()有一点点不同,如果内有属性是不可枚举,那就不会输出,举个例子

1
2
3
const arr = [1, 2, 3];
console.log(Object.getOwnPropertyNames(arr)); // [ '0', '1', '2', 'length' ]
console.log(Object.keys(arr)); // [ '0', '1', '2' ]

ps: Object.keys()for..in结果一致
关于枚举之后再写一篇文章介绍

操作符实现

上面说到了一些操作符newinstanceofin,如果我们自己实现一次会是怎样的呢?

instanceof 实现

instanceof实现是最简单的,不断在原型链上找,找到null那就是到头了

1
2
3
4
5
6
7
8
9
function instanceofFunction(instance, object) {
let proto = instance.__proto__;
const prototype = object.prototype;
while (true) {
if (proto === null) return false;
if (proto === prototype) return true;
proto = proto.__proto__;
}
}

in 实现

in判断其实存在误区,它虽然的作用是所有属性判断,下意识感觉就可以通过foo.bar !== undefined就知道有没有,但如果赋值为undefined就不好弄了

1
2
3
4
5
6
7
function Foo() {
this.bar = undefined;
}

const foo = new Foo();
console.log(foo.bar !== undefined); // false
console.log("bar" in foo); // true

这个时候其实思路和instanceof一致,不断往上调用getOwnPropertyNames来判断是否拥有,就可以实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function inFunction(prop, object) {
// Symbol.iterator in [] // 返回true (数组可迭代,只在ES2015+上有效) 特殊处理
if (prop === Symbol.iterator) {
return !!object[Symbol.iterator];
}
let proto = object;
while (true) {
if (proto === null) return false;
for (const p of Object.getOwnPropertyNames(proto)) {
if (p === prop) return true;
}
proto = proto.__proto__;
}
}

new 实现

new了一个新的实例出来之后,里面包含的其实有 {__proto__, constructor, ...attribute}__proto__和一些属性能理解,但是为什么还包含constructor呢?这个也是我刚刚特意还没提到的东西,实际上实例里面包含了constructor也就是方法本身,用之前的代码来解释就是

1
console.log(foo.constructor === Person); //  true

为什么不是Student呢,因为之前写的比较粗糙,继承了之后应该还要把constructor修正,修正之后就对了

1
2
3
Student.prototype.constructor = Student;

console.log(new Student().constructor === Student); // true

简单实现

知道这些之后我们就可以实现一边new

1
2
3
4
5
6
7
8
9
10
function createObject(constructor) {
const obj = {};
obj.__proto__ = constructor.prototype;
obj.constructor = constructor;
return obj;
}

const object = createObject(Student);
console.log(object.__proto__ === Student.prototype); // true
console.log(object.constructor === Student); // true

绑定属性

这样就完成大部分了,为什么是大部分呢…,因为我们的往上的例子里面,都没有参数,也没有一些基于属性的方法,我们来修改一下

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.name = name;
}

Person.prototype.run = function() {
console.log(`${this.name} running...`);
};

function Student(name, number) {
Person.call(this, name);
this.number = number;
}

Student.prototype = new Person();
Student.prototype.read = function() {
console.log(`student ${this.number} reading...`);
};
Student.prototype.constructor = Student;

const foo = new Student("foo", 1);
foo.run(); // person running...
foo.read(); // student reading...
console.log(foo.name); // foo

但是如果我们这样绑定方法和属性之后,就改成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createObject(constructor, ...args) {
const obj = {};
obj.__proto__ = constructor.prototype;
obj.constructor = constructor;

constructor.call(obj, ...args);
return obj;
}

const bar = createObject(Student, "bar", 2);
console.log(bar.__proto__ === Student.prototype);
console.log(bar.constructor === Student);
bar.run(); // bar running...
bar.read(); // bar student 2 reading...

返回对象

貌似好了,但是其实还有一个问题,我们现在遇到的constructor全部都没有返回值,貌似这样理所当然的,但其实,是可以拥有返回值的,如果返回值不为nullObject或者Function就可以直接返回,取代this,其他返回值无效。
如果这样,加一个判断就搞定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createObject(constructor, ...args) {
const obj = {};
obj.__proto__ = constructor.prototype;
obj.constructor = constructor;

const newObj = constructor.apply(obj, args);
if (
newObj !== null &&
(typeof newObj === "object" || typeof newObj === "function")
) {
return newObj;
}
return obj;
}

ES6 class

ES6 增加了class的概念,其实 JavaScript 历史那么悠久,修修补补一直都用过来了,也不能可能从那么底层的概念更改,所以这其实是一个语法糖,他实际上的并没有和我们用原型链的方法差在哪,用new的例子,改用class写一次

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
// function Person(name) {
// this.name = name;
// }

// Person.prototype.run = function() {
// console.log(`${this.name} running...`);
// };

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

run() {
console.log(`${this.name} running...`);
}
}

// function Student(name, number) {
// Person.call(this, name);
// this.number = number;
// }

// Student.prototype = new Person();
// Student.prototype.read = function() {
// console.log(`student ${this.number} reading...`);
// };
// Student.prototype.constructor = Student;

class Student extends Person {
constructor(name, number) {
super(name);
this.number = number;
}
read() {
console.log(`student ${this.number} reading...`);
}
}

const foo = new Student("foo", 1);
foo.run(); // person running...
foo.read(); // student reading...
console.log(foo.name); // foo

总结

虽然原型链在生产环境上用的还是比较少,但是这却是一道面试很常问的一道问题,学习一下总没坏处,对于自己看源码也有帮助(不过真的好麻烦…),而且从这个过程中也了解到很多不同的东西,比如枚举属性,Array里面的Symbol.iterator等等,再继续查下去还会发现有许多不同的继承方式,这一点之后或许学习然后记录一下。