继承指使某个对象 关联 另一个对象的属性和方法. 大多数编程语言中运用的继承类型为基于类的继承(Classical Inheritance), 而JavaScript中的继承类型为基于原型的继承(Prototypal Inheritance).

原型

假设现在有一个对象obj, obj有prop1属性, 可以通过.操作符获取这个属性值: obj.prop1. 在JavaScript中, 所有对象(包括函数)都有一个隐藏的属性(无法直接获取的属性):原型属性.

原型属性的作用是关联到另一个对象proto(The property is simply a reference to another object proto). proto还能关联另一个proto. 形成了一个原型链(Prototype Chain)

prototype-chain

如上图所示, 我们想通过obj.prop2获取prop2的值, 但obj本身并没有这个属性, 那么就会通过原型链寻找proto中是否有需要的属性.

如果有另一个对象obj2, obj2可以与obj1共享同一个原型对象, 甚至同一条原型链, 如下图, 如果调用obj2.prop2, 返回的是与obj1.prop2同样的属性值, 指向的是内存空间的同一位置.

prototype-prop

可以通过__proto__为对象设置原型对象, 但在 实际应用中不应该这样做,对性能不利.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var person = {
firstname : 'Default',
lastname : 'Default',
getFullName: function(){
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe'
}

// don't do this EVER! for demo purpose only!!!
john.__proto__ = person;
console.log(john.getFullName()); // John Doe

var jane = {
firstname: 'Jane'
}

// share the same proto object
jane.__proto__ = person;
console.log(jane.getFullName()); // Jane Default

一切皆对象

在JavaScript中, 一切皆对象(或原始类型) Everything is an object(or a primitive). 它们都有原型对象, 除了base object(对象的原型对象), 下例中的a.__proto__.

1
2
3
4
5
6
7
8
9
10
var a = {};
var b = function(){};
var c = [];

console.log(a.__proto__); // Object {}
console.log(a.__proto__.__proto__); // null
console.log(b.__proto__); // function () {}
console.log(b.__proto__.__proto__); // Object {}
console.log(c.__proto__); // [Symbol(Symbol.unscopables): Object]
console.log(c.__proto__.__proto__); // Object {}

对象的内省

Reflection: An object can look at itself, listing
and changing its properties and methods.

Reflection: 在传统的面向类环境中,检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)。 – You Don’t Know JS

for-in: 以任意顺序迭代对象中的可枚举属性.

for-in与数组:

1
2
3
4
5
6
7
Array.prototype.myCustomFeature = "cool";

var arr = ['John', 'Jane', 'Jim'];

for(var prop in arr){
console.log(prop + ': ' + arr[prop]);
}

从上述代码看出, 数组的索引实际上是属性名, 但在原型对象中加入自己定义的属性之后, 用for-in也迭代出了该属性, 因此不要在数组中使用for-in, 应该用一般的for循环或forEach.

hasOwnProperty: 用这个方法判断属性是否只存在于对象本身, 而不是在原型链的原型对象中.

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
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function(){
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe'
}

john.__proto__ = person;
john.getFullName(); // "John Doe"

for (var prop in john){
console.log(prop + ': ' + john[prop]);
}

// firstname: John
// lastname: Doe
// getFullName: function(){
// return this.firstname + ' ' + this.lastname;
// }

for (var prop in john){
if(john.hasOwnProperty(prop)){ //运用`hasOwnProperty`方法获取只存在于对象内的属性
console.log(prop + ': ' + john[prop]);
}
}
// firstname: John
// lastname: Doe

Underscore库中的extend方法(运用了对象的内省), 见下例👇, 执行_.extend(john, jane, jim);之后, john就有了jane和jim的属性, 除了exdend方法, 还有extendOwn方法:

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
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function(){
return this.firstname + ' ' + this.lastname;
}
}

var john = {
firstname: 'John',
lastname: 'Doe'
}

var jane = {
address: '111 Main St.',
getFormalFullName: function(){
return this.lastname + ', ' + this.firstname;
}
}

jane.__proto__ = person;

var jim = {
getFirstName: function(){
return firstname;
}
}

_.extend(john, jane, jim);
// _.extendOwn(john, jane, jim);

for (var prop in john){
console.log(prop + ': ' + john[prop]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// extend
firstname: Default
lastname: Default
address: 111 Main St.
getFormalFullName: function (){
return this.lastname + ', ' + this.firstname;
}
getFullName: function (){
return this.firstname + ' ' + this.lastname;
}
getFirstName: function (){
return firstname;
}
// extendOwn
firstname: John
lastname: Doe
address: 111 Main St.
getFormalFullName: function (){
return this.lastname + ', ' + this.firstname;
}
getFirstName: function (){
return firstname;
}

extend方法中首个参数对象接受其后参数对象的属性, 包括后面的对象的原型属性, 而且如果有相同属性, 后面对象的属性会覆盖第一个参数对象属性.

extendOwnextend相似, 但不包括后面对象的原型属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// An internal function for creating assigner functions.
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};

// Extend a given object with all the properties in passed-in object(s).
_.extend = createAssigner(_.allKeys);

// Assigns a given object with all the own properties in the passed-in object(s)
_.extendOwn = _.assign = createAssigner(_.keys);

创建对象

构造器函数与new操作符

1
2
3
4
5
6
function Person(){
this.firstname = 'John';
this.lastname = 'Doe';
}
var john = new Person();
console.log(john)
  1. new 创建一个空对象
  2. 调用Person函数, 新的执行环境被创建
  3. this 指向所创建的空对象
  4. Person内定义的firstname和lastname成为新创建对象的属性
  5. 如果在构造器函数中没有声明返回的值, 返回的就是新创建的对象
1
2
3
4
5
6
7
8
9
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
var john = new Person('John', 'Doe');
console.log(john);

var jane = new Person('Jane', 'Doe');
console.log(jane);

利用构造器函数(Function Constructor)和new创建对象时, 会给创建出的对象自动关联一个原型对象, 上述对象的原型对象是constructor为👇的Object{}:

1
2
3
4
function Person(){
this.firstname = 'John';
this.lastname = 'Doe';
}

__proto__prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(firstname,lastname){
this.firstname = firstname;
this.lastname = lastname;
}

var john = new Person('John','Doe');
var jane = new Person('Jane','Doe');

console.log(john.__proto__ === Person.prototype); // true

// 因为上述声明成立, 所以可以通过下面👇的方法给john的原型对象定义属性

Person.prototype.greet = function(){
console.log('Hello, '+ this.firstname + ' ' + this.lastname);
};


john.greet();

console.log(john.__proto__);

一般利用.prototype给对象定义方法, 在构造器函数中定义属性, 其实也可以直接在构造器函数中定义方法, 但因为在JavaScript中, 函数是对象, 会占用内存空间, 见上例, 如果在构造器内定义greet方法, 那么由这个构造器所创建出的每个对象中都含有greet方法的拷贝, 这样就占用更多的内存空间. 而如果在.prototype内定义方法, 不管创建出多少个对象, 这个方法在内存空间只存在一次.

内置构造器函数

1
2
3
4
5
6
var a = new Number(3);
console.log(a); // Number {[[PrimitiveValue]]: 3}

var b = new String('john');
console.log(b);
// String {0: "j", 1: "o", 2: "h", 3: "n", length: 4, [[PrimitiveValue]]: "john"}

利用内置构造器函数创建出的变量a, b是形式为原始类型的对象, 不同的类型有不同的内置属性和方法, 也可以自己定义属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String.prototype.isLengthGreaterThan = function(limit){
return this.length > limit;
};

console.log('John'.isLengthGreaterThan(3)); // true

// JavaScript会自动将字符串转换为对象, 因此上述声明成立, 但不会自动转换数字
Number.prototype.isPositive = function(){
return this > 0;
};

// console.log(3.isPositive()); // SyntaxError

// 除非必要, 否则不要用构造器函数创建原始类型

var a = Number(3);
console.log(a.isPositive()); // true

var b = 3;

console.log(a == b); // false
console.log(a === b); // true

Object.create与原型继承

用构造器函数所实现的继承是模仿其他语言中的类式继承, 而用Object.create实现的是真正的原型继承.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
firstname: 'Default',
lastname: 'Default',
greet: function(){
return 'Hi ' + this.firstname + ' ' + this.lastname;
}
}

var john = Object.create(person);

console.log(john); // Object {}

john.firstname = 'John';
john.lastname = 'Doe';

console.log(john); // Object {firstname: "John", lastname: "Doe"}

polyfill: 对于不支持某些特性的浏览器, 使用polyfill来实现同样的功能. (Code that adds a feature which the engine may lack.)

Object.create的polyfill简化版:

1
2
3
4
5
6
7
8
9
10
if(!Object.create){
Object.create = function (o){
if(arguments.length > 1){
throw new Error('Object.create implementation only accepts the firstparameter');
}
function F() {};
F.prototype = o;
return new F();
};
}

参考