构造函数_函数原型_实例_实例原型_继承_类

原型

每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。

原型

prototype

每个函数都有一个 prototype 属性,这个属性指向函数的原型对象,这个对象正是调用该构造函数而创建的实例的原型

  • 通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性

__proto__

每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

  • Object.prototype 这个对象是个例外,它的__proto__值为null

constructor

每个原型都有一个 constructor 属性指向关联的构造函数

1
2
3
4
5
6
7
8
9
10
11
function Person() {
}
var person = new Person();

console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型 Object.getPrototypeOf()
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

// 当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor === Person

原型链

每个对象都有一个内部属性[[prototype]],通常称之为原型.原型的值可以是一个对象,也可以是null.如果它的值是一个对象,则这个对象也一定有自己的原型.这样就形成了一条线性的链,称之为原型链.

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

原型链

1
2
3
4
5
6
7
8
function getProperty(obj, prop) {
if(obj.hasOwnProperty(prop) return obj[prop];
else if(obj.__proto__ !== null) return getProperty(obj.__proto__, prop);
else return undefined;
}

# obj.hasOwnProperty 是 JavaScript 中处理属性并且不会遍历原型链的方法之一。
# 另一种方法: Object.keys()
ES5的构造函数生成实例对象的传统方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Point(x, y) {

// 如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined
// if (new.target !== undefined) {
if (new.target === Point) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}

this.x = x;
this.y = y;
this.selfFunc = function() {...};
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
new的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1.以构造器的prototype为原型,创建新对象,从而可以访问原型属性和原型方法

let child = Object.create(Parent.prototype);

Object.create = function (parentObj) {
function F() {};
F.prototype = parentObj;
return new F();
}

或者

let child = new Object();
child.__proto__ = Parent.prototype;
child.constructor = Parent.prototype.constructor;

2.将this和调用参数传给构造器执行,从而设置实例属性和实例方法

let result = Parent.apply(child, rest);

3.如果构造器没有手动返回对象,则返回第一步的对象

return typeof result === 'object' ? result : child;
设置实例属性的一种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object.defineProperty( newObject, "someKey", {
value: "for more control of the property's behavior",
writable: true,
enumerable: true,
configurable: true
});

Object.defineProperties( newObject, {
"someKey": {
value: "Hello World",
writable: true
},
"anotherKey": {
value: "Foo bar",
writable: false
}
});

继承

1. 原型链继承

问题:

  1. 继承的引用类型的属性被所有实例共享
  2. 在创建 Child 的实例时,不能向Parent传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent () {
this.type = "parent";
this.names = ['kevin', 'daisy'];
}
Parent.prototype.getNames = function () {
console.log(this.names);
}
function Child () {
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child();
console.log(child1.getNames()); // ['kevin', 'daisy']
child1.names.push("Clark");
child1.type = "child1";
console.log(child1.type) // child1, child1.type覆盖了原型上type的值
var child2 = new Child();
console.log(child2.getNames()); // ['kevin', 'daisy', 'Clark']
console.log(child2.type); // parent

2. 借用构造函数(经典继承)

优点:

  1. 避免了引用类型的属性被所有实例共享
  2. 可以在 Child 中向 Parent 传参

缺点:

  1. 方法都在构造函数中定义,每次创建实例都会创建一遍方法
  2. 此方法只能继承Parent实例的属性和方法,无法继承Parent原型的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent () {
this.names = ['kevin', 'daisy'];
}
Parent.prototype.getNames = function () {
console.log(this.names);
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
console.log(child2.getNames()); // Uncaught TypeError: child2.getNames is not a function

3. 组合继承 - 组合原型链继承和经典继承

优点:

  1. 融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

缺点:

  1. 会调用两次父构造函数
  2. Child.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
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name); // 借用构造函数继承
this.age = age;
}
// 原型链继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

4. 原型式继承 - 非构造函数继承

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

缺点:

  1. 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
1
2
3
4
5
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}

5. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

缺点:

  1. 跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
1
2
3
4
5
6
7
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}

6. 寄生组合式继承

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();

Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

console.log(child1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 封装后
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

7. 直接只继承prototype - 有缺陷

只继承父构造函数原型的属性和方法

1
2
3
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
// 注意Parent.prototype.constructor也改变了!!!

8. 利用空对象作为中介,只继承prototype

只继承父构造函数原型的属性和方法

1
2
3
4
5
6
7
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}

9. 拷贝继承

只继承父构造函数原型的属性和方法

1
2
3
4
5
6
7
8
function copyExtend(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}

10. 浅拷贝 - 非构造函数继承

存在父对象被篡改的可能

1
2
3
4
5
6
7
8
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}

11. 深拷贝 - 非构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
ES6引入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
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
let methodName = "someFunc";
// 与函数一样,类也可以使用表达式的形式定义。
// const GlobalPointName = class Point { // 类的表达式中,Point只能类内部使用
class Point {
// 实例属性
x=0;
y=0;

constructor(x, y) {
// this 关键字代表实例对象
this.x = x || this.x;
this.y = y || this.y;

console.log(new.target === Point) // true

if (new.target === Point) {
throw new Error('本类不能实例化');
}

}

// 原型方法
toString() {
return '(' + this.x + ', ' + this.y + ')';
}

// 取值函数(getter)和存值函数(setter)
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}

// 类的属性名,可以采用表达式
[methodName]() {...}

// 静态方法
// 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。
// 静态方法可以与非静态方法重名。
// 父类的静态方法,可以被子类继承。
// 静态方法也是可以从super对象上调用的。
static staticFunc() {...}

// 静态属性提案
static staticProp = 1;

// 私有属性提案
// 私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。
#count = 0;

// 私有方法提案
#privateFunc() {}
}

// 静态属性
Point.staticProp = 1;

// 类的继承
class Line extends Point {
constructor(x, y) {
// 必须调用super来实例化子类自己的this
// 父类中的new.target会返回Line
super(x, y);
}

static staticFunc() {
return super.staticFunc() + ', too';
}
}

let p = new Point(1, 2);
p.prop = 123; // setter: 123
p.prop // 'getter'
Point.staticFunc();

// ES6中的class是构造函数的另一种写法
typeof Point // "function"

Point === Point.prototype.constructor // true
p.constructor === Point.prototype.constructor // true

// 事实上,类的所有方法都定义在类的prototype属性上面。

// 另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)

// 在类的实例上面调用方法,其实就是调用原型上的方法。

// Object.assign方法可以很方便地一次向类添加多个方法
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});

// constructor方法在new命令执行时自动被调用
// constructor方法默认返回实例对象,即this
// 类必须使用new调用,否则会报错
class Point {
}
// 等同于
class Point {
constructor() {}
}

p.hasOwnProperty('x') // true
p.hasOwnProperty('y') // true
p.hasOwnProperty('toString') // false
p.__proto__.hasOwnProperty('toString') // true
p.__proto__.hasOwnProperty('x') // false

// 与 ES5 一样,类的所有实例共享一个原型对象
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ === Point.prototype //true


var p1 = new Point(2,3);
var p2 = new Point(3,2);
// p1.__proto__.printName = function () { return 'Oops' };
Object.getPrototypeOf(obj).printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"

// 存值函数和取值函数是设置在属性的 Descriptor 对象上的。
var descriptor = Object.getOwnPropertyDescriptor(
Point.prototype, "prop"
);
"get" in descriptor // true
"set" in descriptor // true

# 类和模块的内部,默认就是严格模式
# 类不存在变量提升
# Point.name // 'Point'
# *[Symbol.iterator] () {...}
# 类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

# 一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}

# 另一种解决方法是使用箭头函数。
# 箭头函数内部的this总是指向定义时所在的对象。
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true

# 还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());

# 私有方法和私有属性
# 一种方法就是命名时前面加_,只便于阅读
# 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) { // bar即为私有方法
return this.snaf = baz;
}
# 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
# bar和snaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
ES6继承
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
ES6 的继承机制实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。 

class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
// 相当于
// Point.prototype.constructor.call(this, ...args)
// super作为对象时,在普通方法中,指向父类的原型对象(ParentClass.prototype);在静态方法中,指向父类。
// 这里需要注意,在普通方法中,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性(this.prop=xxx),是无法通过super调用的。
// ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例
// 由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
// 另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
// 注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
}
}

# 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

# 父类的静态方法也会被子类继承

# 判断继承关系
Object.getPrototypeOf(ColorPoint) === Point // true

class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined, 实际为A.prototype.x
console.log(this.x); // 3
}
}
let b = new B();
ES6类的prototype 属性和__proto__属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

1
2
3
4
5
6
7
8
9
class A {
}
class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
B.prototype // {constructor: class B, __proto__: class A}
A.prototype // {constructor: class A, __proto__: Object}

这样的结果是因为,类的继承是按照下面的模式实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
}
class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();


Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}

这两条继承链,可以这样理解:

  • 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);
  • 作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(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
class B extends A {
}

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。
由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

讨论2种特殊情况

第一种,子类继承Object类。

class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。


第二种情况,不存在任何继承。

class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。
ES6子类的实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。

1
2
3
4
5
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

1
2
3
4
5
6
7
8
9
Boolean()  
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

以前,这些原生构造函数是无法继承的。
原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。

ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

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
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]

x.commit();
x.history // [[], [1, 2]]

x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert();
x // [1, 2]

Mixin模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

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
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}

for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}

return Mix;
}

function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}


class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}