原型
原型是 JavaScript 面向对象特性中重要的概念,也是大家太熟悉的概念。因为在绝大多
数的面向对象语言中,对象是基于类的(例如 Java 和 C++ ) ,对象是类实例化的结果。而在
JavaScript 语言中,没有类的概念
① ,对象由对象实例化。打个比方来说,基于类的语言中类
就像一个模具,对象由这个模具浇注产生,而基于原型的语言中,原型就好像是一件艺术品
的原件,我们通过一台 100% 精确的机器把这个原件复制出很多份。
前面小节的例子中都没有涉及原型,仅仅通过构造函数和 new 语句生成类,让我们看
看如何使用原型和构造函数共同生成对象。
function Person() {}
Person.prototype.name = ‘BYVoid’;
Person.prototype.showName = function() {
console.log(this.name);
};
var person = new Person();
person.showName();
上面这段代码使用了原型而不是构造函数初始化对象。 这样做与直接在构造函数内定义
属性有什么不同呢?
**构造函数内定义的属性继承方式与原型不同, 子对象需要显式调用父对象才能继承构
造函数内定义的属性。
构造函数内定义的任何属性, 包括函数在内都会被重复创建, 同一个构造函数产生的
两个对象不共享实例。**
构造函数内定义的函数有运行时闭包的开销, 因为构造函数内的局部变量对其中定义
的函数来说也是可见的。
下面这段代码可以验证以上问题:
function Foo() {
var innerVar = ‘hello’;
this.prop1 = ‘BYVoid’;
this.func1 = function() {
innerVar = ”;
};
}
Foo.prototype.prop2 = ‘Carbo’;
Foo.prototype.func2 = function() {
console.log(this.prop2);
};
var foo1 = new Foo();
var foo2 = new Foo();
console.log(foo1.func1 == foo2.func1); // 输出 false
console.log(foo1.func2 == foo2.func2); // 输出 true
尽管如此,并不是说在构造函数内创建属性不好,而是两者各有适合的范围。那么我们
什么时候使用原型,什么时候使用构造函数内定义来创建属性呢?
**除非必须用构造函数闭包,否则尽量用原型定义成员函数,因为这样可以减少开销。
尽量在构造函数内定义一般成员, 尤其是对象或数组, 因为用原型定义的成员是多个
实例共享的。**
接下来,我们介绍一下JavaScript中的原型链机制。
原型链
JavaScript 中有两个特殊的对象: Object 与 Function ,它们都是构造函数,用于生
成对象。 Object.prototype 是所有对象的祖先, Function.prototype 是所有函数的原
型,包括构造函数。我把 JavaScript 中的对象分为三类,
一类是用户创建的对象,
一类是构造函数对象,
一类是原型对象。
用户创建的对象,即一般意义上用 new 语句显式构造的对象。
构造函数对象指的是普通的构造函数,即通过 new 调用生成普通对象的函数。
原型对象特指构造函数 prototype 属性指向的对象。
这三类对象中每一类都有一个 proto 属性,它指向该对象的原型,从任何对象沿着它开始遍历都可以追溯到 Object.prototype 。
构造函数对象有 prototype 属性,指向一个原型对象,通过该构造函数创建对象时,被创建对象的 proto 属性将会指向构造函数的 prototype 属性。
原型对象有 constructor属性,指向它对应的构造函数。让我们通过下面这个例子来理解原型:
function Foo() {}
Object.prototype.name = ‘My Object’;
Foo.prototype.name = ‘Bar’;
var obj = new Object();
var foo = new Foo();
console.log(obj.name); // 输出 My Object
console.log(foo.name); // 输出 Bar
console.log(foo.proto.name); // 输出 Bar
console.log(foo.proto.proto.name); // 输出 My Object
console.log(foo.proto.constructor.prototype.name); // 输出 Bar
我们定义了一个叫做 Foo () 的构造函数,生成了对象 foo 。同时我们还分别给 Object和 Foo 生成原型对象。
下图解析了它们之间错综复杂的关系。
对象的复制
JavaScript 和 Java 一样都没有像C语言中一样的指针,所有对象类型的变量都是指向对
象的引用,两个变量之间赋值传递一个对象并不会对这个对象进行复制,而只是传递引用。
有些时候我们需要完整地复制一个对象,这该如何做呢? Java 语言中有 clone 方法可以实
现对象复制,但 JavaScript 中没有这样的函数。因此我们需要手动实现这样一个函数,一个
简单的做法是复制对象的所有属性:
Object.prototype.clone = function() {
var newObj = {};
for (var i in this) {
newObj[i] = this[i];
}
return newObj;
}
var obj = {
name: ‘byvoid’,
likes: [‘node’]
};
var newObj = obj.clone();
obj.likes.push(‘python’);
console.log(obj.likes); // 输出 [ ‘node’, ‘python’ ]
console.log(newObj.likes); // 输出 [ ‘node’, ‘python’ ]
上面的代码是一个对象浅拷贝(shallow copy)的实现,即只复制基本类型的属性,而
共享对象类型的属性。 浅拷贝的问题是两个对象共享对象类型的属性, 例如上例中 likes 属
性指向的是同一个数组。
实现一个完全的复制,或深拷贝(deep copy)并不是一件容易的事,因为除了基本数据
类型,还有多种不同的对象,对象内部还有复杂的结构,因此需要用递归的方式来实现:
Object.prototype.clone = function() {
var newObj = {};
for (var i in this) {
if (typeof(this[i]) == ‘object’ || typeof(this[i]) == ‘function’) {
newObj[i] = this[i].clone();
} else {
newObj[i] = this[i];
}
}
return newObj;
};
Array.prototype.clone = function() {
var newArray = [];
for (var i = 0; i < this.length; i++) {
if (typeof(this[i]) == ‘object’ || typeof(this[i]) == ‘function’) {
newArray[i] = this[i].clone();
} else {
newArray[i] = this[i];
}
}
return newArray;
};
Function.prototype.clone = function() {
var that = this;
var newFunc = function() {
return that.apply(this, arguments);
};
for (var i in this) {
newFunc[i] = this[i];
}
return newFunc;
};
var obj = {
name: ‘byvoid’,
likes: [‘node’],
display: function() {
console.log(this.name);
},
};
var newObj = obj.clone();
newObj.likes.push(‘python’);
console.log(obj.likes); // 输出 [ ‘node’ ]
console.log(newObj.likes); // 输出 [ ‘node’, ‘python’ ]
console.log(newObj.display == obj.display); // 输出 false
上面这个实现看起来很完美,它不仅递归地复制了对象复杂的结构,还实现了函数的深
拷贝。这个方法在大多数情况下都很好用,但有一种情况它却无能为力,例如下面的代码:
var obj1 = {
ref: null
};
var obj2 = {
ref: obj1
};
obj1.ref = obj2;
这段代码的逻辑非常简单,就是两个相互引用的对象。当我们试图使用深拷贝来复制
obj1 和 obj2 中的任何一个时,问题就出现了。因为深拷贝的做法是遇到对象就进行递归
复制,那么结果只能无限循环下去。对于这种情况,简单的递归已经无法解决,必须设计一
套**图论算法, 分析对象之间的依赖关系, 建立一个拓扑结构图, 然后分别依次复制每个顶点,
并重新构建它们之间的依赖关系**。