在基于类的传统面向对象的编程语言中,对象由类实例化而来,实例化的过程中,类的属性和方法会拷贝到这个对象中;对象的继承实际上是类的继承,在定义子类继承于父类时,子类会将父类的属性和方法拷贝到自身当中。因此,这类语言中,对象创建和继承行为都是通过拷贝完成的。但在JavaScript中,对象的创建、对象的继承(更好的叫法是对象的代理,因为它并不是传统意义上的继承)是不存在拷贝行为的。(ES6 中的class也只不过是语法糖,并非真正意义上的类)

一、 原型

在JavaScript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型prototype,正如下面所展示的。原型是为了共享多个对象之间的一些共有特性(属性或方法),这个功能也是任何一门面向对象的编程语言必须具备的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function doSomething(){}
console.log( doSomething.prototype );

// doSomething 函数有一个默认的原型属性,它在控制台上面呈现了出来.
// 运行这段代码之后,控制台上面应该出现了像这样的一个对象.

{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}

二、 原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

JavaScript中的对象,都有一个内置属性[[Prototype]],指向这个对象的原型对象。当查找一个属性或方法时,如果在当前对象中找不到定义,会继续在当前对象的原型对象中查找;如果原型对象中依然没有找到,会继续在原型对象的原型中查找(原型也是对象,也有它自己的原型);如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。可以看出,这个查找过程是一个链式的查找,每个对象都有一个到它自身原型对象的链接,这些链接组建的整个链条就是原型链。拥有相同原型的多个对象,他们的共同特征正是通过这种查找模式体现出来的。

doSomething的例子中,我们可以看到最顶层的原型对象,这个对象就是Object.prototype,这个对象中保存了最常用的方法,如toString、valueOf、hasOwnProperty等,因此我们才能在任何对象中使用这些方法。

三、 prototype

prototype 属性:继承成员被定义的地方

在 JavaScript 原型继承结构里面,规范中用 [[Prototype]] 表示对象隐式的原型,在 JavaScript 中用 __proto__ 表示,在 Firefox 和 Chrome 浏览器中是可以访问得到这个属性的,在 IE 下不行。所有 JavaScript 对象都有 __proto__ 属性,但只有 Object.prototype.__proto__null ,前提是没有修改过这个属性。这个属性指向它的原型对象。 至于显示的原型,在 JavaScript 里用 prototype 属性表示。

四、 constructor

每个实例对象都从原型中继承了一个 constructor 属性,该属性指向了用于构造此实例对象的构造函数。

一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function deepClone(obj) { //递归拷贝
if(obj === null) return null; //null 的情况
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(typeof obj !== 'object') {
//如果不是复杂数据类型,直接返回
return obj;
}
/**
* 如果obj是数组,那么 obj.constructor 是 [Function: Array]
* 如果obj是对象,那么 obj.constructor 是 [Function: Object]
*/
let t = new obj.constructor();
for(let key in obj) {
//如果 obj[key] 是复杂数据类型,递归
t[key] = deepClone(obj[key]);
}
return t;
}

五、 注意事项

对象的原型(可以通过Object.getPrototypeOf()或者已被弃用的__proto__属性获得)与构造函数的 prototype 属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。

1
2
3
4
Object.getPrototypeOf(new String()) === String.prototype // true
Object.getPrototypeOf(new Number()) === Number.prototype // true
Object.getPrototypeOf(new Object()) === Object.prototype // true
Object.getPrototypeOf(new Array()) === Array.prototype // true

Q: 如何创造一个既没有属性方法也没有原型链的空对象?

1
2
3
4
let nullObject = Object.create(null);
// {} No properties
nullObject.__proto__; // undefined
nullObject.prototype; // undefined


参考文章: