老姚谈:JavaScript 中 prototype 的本质

1,753 阅读4分钟
原文链接: zhuanlan.zhihu.com

来源:prototype的本质

作者:老姚(已获得作者授权许可)


在一篇文章里提到了,原型的本质就是一种委托关系
即:我这里没有,就到我的原型里去看看,一旦找到就当成我的用。
本文详细说一下这个事情。
比如某女买东西,钱都是她老公付款的。
用程序刻画是这样的:
var girl = {
	name: '小美'
};
var boy = {
	name: '小帅',
	pay: function() {
		console.log('花了一千元');
	}
};
Object.setPrototypeOf(girl, boy);
girl.pay();

程序中指明了girl的原型是boy,girl没pay方法,但是boy有,所以boy花钱了。
从这个例子来看那么,原型是一种委托关系,如果说是一种继承关系就不是那么贴切。
因为这段代码更等价于如下的代码:

var girl = {
	name: '小美',
	pay: function() {
		boy.pay();
	}
};
var boy = {
	name: '小帅',
	pay: function() {
		console.log('花了一千元');
	}
};
girl.pay();

用这种委托关系,而不是继承关系去理解原型,会感觉一切豁然开朗。
我们通过下面这个例子来看看什么是原型链?

var a = {
	fn1: function() {
		console.log(1);
	}
};
var b = {
	fn2: function() {
		console.log(2);
	}
};
var c = {
	fn3: function() {
		console.log(3);
	}
};
var d = {
	fn4: function() {
		console.log(4);
	}
};
Object.setPrototypeOf(d, c);
Object.setPrototypeOf(c, b);
Object.setPrototypeOf(b, a);
d.fn1();
d.fn2();
d.fn3();
d.fn4();

上面的代码中,a是b的原型,b是c的原型,c又是d的原型。
那么d要找fn1方法,怎么找呢?
先去其原型c中找,没找到,
再去c的原型b中找,也没找到,
再去b的原型a中去找,找到了,
因此能调用fn1方法。
上面的过程就是原型链查找的过程。

讲到这里,原型链的原理想必是懂了。
此时我们再来看a,b,c,d四个对象是什么关系。
如果要看做是继承的话,那么就是父子关系。
如果用委托观点来看,那么每一个对象,都是后一个对象的智囊,也就是原型。
所以本文的观点是什么呢?
不要把原型当成亲爹,要当成智囊,要当成老公,要当成干爹,
本质是委托关系,说白了,就是“利用”。

其实讲到这儿原型是什么基本已经说完,后面我准备展开说说,跟构造函数扯上。

第一个问题,什么叫“一旦找到就当成我的用”?
其实指的是this问题。
比如:

var a = {
	sayName: function() {
		alert(this.name);
	}
};
var laoyao = {
	name: 'laoyao'
};
Object.setPrototypeOf(laoyao, a);
laoyao.sayName();

第二个问题,克隆的观点?
假如一个对象是一个空对象的原型,
因为空对象什么也没有,所有的都来自其原型,
我们可以认为此对象是其原型的克隆。

var laoyao = {
	name: 'laoyao',
	sayName: function() {
		alert(this.name);
	}
};
var fenshen = {};
Object.setPrototypeOf(fenshen, laoyao);
console.log(fenshen.name);
fenshen.sayName();

当然,Object.create更适合描述克隆。

var laoyao = {
	name: 'laoyao',
	sayName: function() {
		alert(this.name);
	}
};
var fenshen = Object.create(laoyao);
console.log(fenshen.name);
fenshen.sayName();

这里只使用了create的第一个参数,对此,这段代码很其上一段代码没什么区别。

第三,我们封装产生对象的过程?
我们希望上述的那个laoyao对象能通过函数产生。比如我会这样做:

var createPerson = function(name) {
	return {
		name: name,
		sayName: function() {
			alert(this.name)
		}
	};
}
var laoyao = createPerson('laoyao');
laoyao.sayName();

我也换种方式来做:

var createPerson = function(name) {
	
	var o = {};
	o.name = name;
	
	var proto = {
		sayName: function() {
			alert(this.name);
		}
	};
	
	Object.setPrototypeOf(o, proto);
	return o;
}
var laoyao = createPerson('laoyao');
laoyao.sayName();

我再继续变化,把proto拿出来

var createPerson = function(name) {
	var o = {};
	o.name = name;
	Object.setPrototypeOf(o, createPerson.proto);
	return o;
}
createPerson.proto = {
	sayName: function() {
		alert(this.name);
	}
};
var laoyao = createPerson('laoyao');
laoyao.sayName();

写到这里,你会发现其实跟我们平常写的代码很像:

var Person = function(name) {
	this.name = name;
}
Person.prototype = {
	sayName: function() {
		alert(this.name);
	}
};
var laoyao = new Person('laoyao');
laoyao.sayName();

我们来刻画一下这个new过程。
我们假设new是一个函数,类似call或bind的那样的函数

new Person('laoyao')

我们换成

Person.new('laoyao')

此函数定义如下:

Function.prototype.new = function() {
	var that = Object.create(this.prototype);
	this.apply(that, arguments);
	return that;
};
var Person = function(name) {
	this.name = name;
}
Person.prototype = {
	sayName: function() {
		alert(this.name);
	}
};

var laoyao = Person.new('laoyao');
laoyao.sayName();

其中这new函数,不是完整模拟new的(考虑返回值是否是对象)。详细请看《js语言精粹》47页。其中this指向的是当前函数(Person)。
如果上面的代码看不习惯的话,我们也可以发明如下的api:

myNew(Person, 'laoyao')

完整示例代码如下:

var myNew = function() {
	var Constructor = [].shift.call(arguments);
	var that = Object.create(Constructor.prototype);
	Constructor.apply(that, arguments);
	return that;
}
var Person = function(name) {
	this.name = name;
}
Person.prototype = {
	sayName: function() {
		alert(this.name);
	}
};
var laoyao = myNew(Person, 'laoyao');
laoyao.sayName();

上述代码改编于《JS设计模式与开发实践》第20页。

如果读者没有其他什么面向对象语言基础,
那么看此文,会觉得new是一个封装委托关系的过程。
而不是什么模拟java,模拟不彻底啥的。
所以在我看来,那些号召不要使用new的文章,也未必正确。

最后说下:这个委托的观点,《你不知道的javascript》有更详细的介绍。

也欢迎关注本文续:老姚谈:JavaScript中prototype的本质(二)

本文完。