原型与继承

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型。

当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。

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
'use strict';

const user = {
name: 'user',
age: 18
}
const admin = {
__proto__: user, // __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。一个对象只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。
name: 'admin',
}
console.log(admin.name); // admin
console.log(admin.age); // 18

console.log(admin.__proto__); // { name: 'user', age: 18 }
console.log(admin.__proto__.__proto__); // [Object: null prototype] {}
console.log(admin.__proto__.__proto__.__proto__); // null
// 获取对象所有的属性名, 包括不可迭代的属性
console.log(Object.getOwnPropertyNames(admin.__proto__.__proto__));
/*
[
'constructor',
'__defineGetter__',
'__defineSetter__',
'hasOwnProperty',
'__lookupGetter__',
'__lookupSetter__',
'isPrototypeOf',
'propertyIsEnumerable',
'toString',
'valueOf',
'__proto__',
'toLocaleString'
]
*/
// 只返回自己的 key
console.log(Object.keys(admin)); // [ 'name' ]
// for..in 会遍历自己以及继承的键
for(let key in admin) console.log(key); // name age
// 方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true。
console.log(admin.hasOwnProperty("name")); // true
console.log(admin.hasOwnProperty("age")); // false
  • __proto__[[Prototype]] 的因历史原因而留下来的 getter/setter

  • 初学者常犯一个普遍的错误,就是不知道 __proto__[[Prototype]] 的区别。

  • 请注意,__proto__ 与内部的 [[Prototype]] 不一样__proto__[[Prototype]] 的 getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。

  • __proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。稍后我们将介绍这些函数。

  • 根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。

  • 由于 __proto__ 标记在观感上更加明显,所以我们在后面的示例中将使用它。

1
2
3
4
5
6
7
8
9
10
11
12
let user = {
setName(name){
this.name = name
}
}
let admin = {
__proto__: user
}
// 即使是调用的继承对象user中的方法, 但是方法中的this依然指向调用者admin
admin.setName("admin");
console.log(admin.name); // admin
console.log(user.name); // undefind

我们还记得,可以使用诸如 new F() 这样的构造函数来创建一个新对象。

如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict";

MyArray.prototype = new Array(); // * 与使用 ** 行, ***行 等效
// Array.prototype = Array.prototype // **
function MyArray() {
// this.__proto__ = new Array(); // ***
// this 是一个new出来的具体对象, MyArray 是一个构造函数
}

const arr = new MyArray();
// MyArray.prototype上的属性方法可直接使用, 未找到会自动向上寻找, 在`arr.__proto__`上找到constructor方法
console.log(arr.constructor === Array); // true
console.log(arr.__proto__.constructor === Array); // true
// 调用Array.prototype 上的方法
arr.push(...[1, 2, 3]);
console.log(arr);

每个函数都有 "prototype" 属性,即使我们没有提供它。

默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"use strict";

function User() {}

console.log(Object.getOwnPropertyDescriptors(User.prototype));
/* {
constructor: {
value: [Function: User],
writable: true,
enumerable: false,
configurable: true
}
} */
console.log(User.prototype.constructor === User); // true
console.log(User.prototype.__proto__.constructor === Object); // true
console.log(User.prototype.__proto__.__proto__ === null); // true, null没有构造方法

const user = new User();
console.log(user.__proto__ === User.prototype); // true
console.log(user.__proto__.__proto__.__proto__ === null); // true, 原型链的尽头是null
// User.prototype 上的属性可直接使用
console.log(user.constructor === User); // true

在常规对象上,prototype 没什么特别的:

1
2
3
4
let user = {
name: "John",
prototype: "Bla-bla" // 这里只是普通的属性
};

默认情况下,所有函数都有 F.prototype = {constructor:F},所以我们可以通过访问它的 "constructor" 属性来获取一个对象的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"use strict";
function User(name){
this.name = name;
}
// 构造方法应加上关键字new调用
let user1 = new User("User1");
console.log(user1);

let user2 = new User.prototype.constructor("User2");
console.log(user2);

let user3 = new user1.__proto__.constructor("User3");
console.log(user3);

let user4 = new user1.constructor("User4");
console.log(user4); // User { name: 'User4' }

User.prototype = {}
let user5 = new user1.constructor("User5");
console.log(user5); // { name: 'User5' } 是 new Object('User5')的结果, 继续向上找constructor

user1.__proto__ = null // user1.constructor is not a constructor, 如果给{}, 结果是 [String: 'User6']
let user6 = new user1.constructor("User6");
console.log(user6);

内建对象的原型挂载着相关属性和方法

可向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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 通过原型查看数组, 字符串等相关的函数(可在浏览器环境下直接使用String.prototype)

// console.log(Object.getOwnPropertyDescriptors(Array.prototype));
console.log(Object.getOwnPropertyNames(Array.prototype));
/* [
'length', 'constructor', 'concat',
'copyWithin', 'fill', 'find',
'findIndex', 'lastIndexOf', 'pop',
'push', 'reverse', 'shift',
'unshift', 'slice', 'sort',
'splice', 'includes', 'indexOf',
'join', 'keys', 'entries',
'values', 'forEach', 'filter',
'flat', 'flatMap', 'map',
'every', 'some', 'reduce',
'reduceRight', 'toLocaleString', 'toString',
'at'
] */
// 所有函数或属性名
console.log(Object.getOwnPropertyNames(String.prototype));
/* [
'length', 'constructor', 'anchor',
'big', 'blink', 'bold',
'charAt', 'charCodeAt', 'codePointAt',
'concat', 'endsWith', 'fontcolor',
'fontsize', 'fixed', 'includes',
'indexOf', 'italics', 'lastIndexOf',
'link', 'localeCompare', 'match',
'matchAll', 'normalize', 'padEnd',
'padStart', 'repeat', 'replace',
'replaceAll', 'search', 'slice',
'small', 'split', 'strike',
'sub', 'substr', 'substring',
'sup', 'startsWith', 'toString',
'trim', 'trimStart', 'trimLeft',
'trimEnd', 'trimRight', 'toLocaleLowerCase',
'toLocaleUpperCase', 'toLowerCase', 'toUpperCase',
'valueOf', 'at'
] */
console.log(Object.getOwnPropertyNames(Number.prototype));
/* [
'constructor',
'toExponential',
'toFixed',
'toPrecision',
'toString',
'valueOf',
'toLocaleString'
] */
console.log(Object.getOwnPropertyNames(Boolean.prototype));
/* [ 'constructor', 'toString', 'valueOf' ] */

console.log(Object.getOwnPropertyNames(Date.prototype));
/* [
'constructor', 'toString', 'toDateString',
'toTimeString', 'toISOString', 'toUTCString',
'toGMTString', 'getDate', 'setDate',
'getDay', 'getFullYear', 'setFullYear',
'getHours', 'setHours', 'getMilliseconds',
'setMilliseconds', 'getMinutes', 'setMinutes',
'getMonth', 'setMonth', 'getSeconds',
'setSeconds', 'getTime', 'setTime',
'getTimezoneOffset', 'getUTCDate', 'setUTCDate',
'getUTCDay', 'getUTCFullYear', 'setUTCFullYear',
'getUTCHours', 'setUTCHours', 'getUTCMilliseconds',
'setUTCMilliseconds', 'getUTCMinutes', 'setUTCMinutes',
'getUTCMonth', 'setUTCMonth', 'getUTCSeconds',
'setUTCSeconds', 'valueOf', 'getYear',
'setYear', 'toJSON', 'toLocaleString',
'toLocaleDateString', 'toLocaleTimeString'
] */
console.log(Object.getOwnPropertyNames(Map.prototype));
/* [
'constructor', 'get',
'set', 'has',
'delete', 'clear',
'entries', 'forEach',
'keys', 'size',
'values'
] */
console.log(Object.getOwnPropertyNames(WeakMap.prototype));
/* [ 'constructor', 'delete', 'get', 'set', 'has' ] */

console.log(Object.getOwnPropertyNames(Function.prototype));
/* [
'length', 'name',
'arguments', 'caller',
'constructor', 'apply',
'bind', 'call',
'toString'
] */
console.log(Object.getOwnPropertyNames(Object.prototype));
/* [
'constructor',
'__defineGetter__',
'__defineSetter__',
'hasOwnProperty',
'__lookupGetter__',
'__lookupSetter__',
'isPrototypeOf',
'propertyIsEnumerable',
'toString',
'valueOf',
'__proto__',
'toLocaleString'
] */
console.log(Object.getOwnPropertyNames(Object));
/* [
'length',
'name',
'prototype',
'assign',
'getOwnPropertyDescriptor',
'getOwnPropertyDescriptors',
'getOwnPropertyNames',
'getOwnPropertySymbols',
'is',
'preventExtensions',
'seal',
'create',
'defineProperties',
'defineProperty',
'freeze',
'getPrototypeOf',
'setPrototypeOf',
'isExtensible',
'isFrozen',
'isSealed',
'keys',
'entries',
'fromEntries',
'values',
'hasOwn'
] */

如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]

通常我们只在创建对象的时候设置它一次,自那之后不再修改

现代的获取/设置原型的方法有:

Object.create(null)这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){...} 还要简单。

缺点是这样的对象没有任何内建的对象的方法,例如 toString

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';
// 这个对象没有原型([[Prototype]] 是 null)
// 创建一个以null为原型的对象, 在obj中, __ptoto__、toString等原本的内建属性和方法不复存在, 只是普通的方法和属性
let obj = Object.create(null);

console.log(obj.__proto__); // undefined
// console.log(obj.toString()); // TypeError: obj.toString is not a function

// 正常的
let obj1 = {};
console.log(obj1.__proto__); // [Object: null prototype] {}
console.log(obj1.toString()); // [object Object]