JavaScript之忧

从过程到类,再到原型链,思绪有没有跟上?

1 引子

经历过面向类编程的教导,来到JavaScript的世界并不会太过新奇。从习惯了的数据类型,到函数,写个小程序不成问题。

JSON格式想必也已熟知,Python把我们教育的很好。所以,JS里的Key-Value对也是能轻车熟路。

了解前端API,引几个外部包,便能尝试前端妖艳;熟悉后台API,npm几个脚手架,便可体味后台酸爽。

当天色暗下,梦里辗转。prototype__proto__,一堆名词背后,是如何支撑这宏伟的JS大厦?

2 忧之源

和其他面向对象语言类似,JS提供了基础数据类型(number、string、boolean),复合数据类型(object),函数(function)。这似乎没什么新鲜的,基本能接受。

> typeof 123
'number'
> typeof 'abc'
'string'
> typeof true
'boolean'
> typeof {}
'object'
> typeof function(){}
'function'
> typeof sfdsafdsa
'undefined'

但也和其他的面向对象套路不太一样:

  • 没有字典(dict)类型
  • 没有数组/列表(list)类型
  • 没有类(class)
  • 没有继承(extend)

而实践早已告诉我们,object+function几乎可以满足我们所有的期待。

// 我像dict,但其实是object
var person1 = {
  name: 'lily'
};

// 我像数组,但其实是object
var array = [1, 2, 3];

// 我像类,但其实是function
var Person = function(name) {
  this.name = name;
};
Person.prototype.say = function() {
  return this.name;
};
var person2 = new Person('lucy');
person2.say(); // lucy

对于初识JS的朋友来说,想必会是扭曲的。明明没有类(ES6之前),但却给一通类里的概念,比如new。还搞出了新的花样,prototype是个什么鬼。

3 忧之思

3.1 模块化

从过程,到类,再到原型,先贤们总能搞出一堆概念帮助我们度过难关。所谓难者,便是维护、扩展。

千变万化也不过是0与1的幻化,而01显然难以维护和扩展。

所以,我们曾爱慕C语言的函数,钟情C++的类。说来说去,这戏法能帮我们组织代码,实现封装。只要我们对职责的划分不那么烂,便可咬着牙把系统维护下去了。

高大上的概念在帮我们实现模块化,让系统增加一丢丢的可维护和可扩展性。

3.2 对象(object)

对象已经成为面向对象语言的标配。不管什么事儿,先定义一个类总会错不了吧。对象,也是在帮助我们实现模块化。

定义一两个对象还行,定义一堆对象就有点麻烦了。所以类(class)横空出世,作为对象的模子,带有继承、多态的特效,加上for语句,new百八十个对象不成问题。

而JS里并没有类,那咋把对象new出来呢?

实践告诉我们,JS里的function可以理解为类,我们可以通过new一个function来生成object。

3.3 函数(function)

如果函数可以代替类,那函数需要具备什么素质呢?成员变量必不可少,如this.namethis.say

// 第一种方法
var Person = function(name) {
  this.name = name;
  this.say = function() {
    console.log(this.name);
  };
};
var person1 = new Person('lucy');
person1.say(); // lucy

好吧,就当是先贤们偷了个懒,把class写成了function。

诡异的是,大家更多的使用以下写法。

// 第二种方法
var Person = function(name) {
  this.name = name;
};
Person.prototype.say = function() {
  console.log(this.name);
};
var person1 = new Person('lucy');
person1.say(); // lucy

我们需要一个解释。既然两者都能实现相同的功能,第二种有什么好处?

3.4 原型(prototype)

第一种与第二种的不同之处在于,成员变量(函数)定义在哪。第一种是定义在函数里边,第二种是定义在函数外边。

定义在哪有问题么?

有。

所谓的定义,其实就是new一块内存空间。在C++里,函数不能定义在函数里边,所以函数不能new一个函数。

而JS不同,函数里是可以new一个函数。

var a = function() {
  var b = function() {
  }
}

所以,如果我们使用第一种方法new出N个person实例,则会new出N个函数this.say = function() {...}。在内存里申请N块区域,存储相同的函数,我们是有多豪啊!

prototype是来拯救我们的,只需把要共享的变量、函数放到它下边,即第二种方法。

既然放到this下和prototype下都行,那同时定义this.namePerson.prototype.name会出现什么情况呢?

var Person = function(name) {
  this.name = name;
};
Person.prototype.name = 'default';
Person.prototype.say = function() {
  console.log(this.name);
};
var person1 = new Person('lucy');
person1.say(); // lucy

答案是,__proto__的值会屏蔽掉prototype的值。

3.5 原型链

果然,object+function满足了我们的所有期待。

一个值得讨论的问题是,new得到的object,是如何找到生成它的函数的。即,person1是如何知道是Person生成了它,而不是Animal。

可以肯定的是,person1一定携带某个信息,这个信息将person1链接到Person,这便是__proto__

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

然后接下来一个问题是,为什么要链接到Person.prototype,而不是Person?因为链接到Person没意义,如果想获取this的值,直接用person1就行。所以,根本的诉求还是要获取prototype的值,那就直接链到它呗。

冥冥之中,有一条链子。对于object来说,__proto__会链接到生成它函数的原型。当访问object[key]的时候,会先看object本身有没有这个key,如果没有再看prototype有没有。这种选择性的看,为继承、多态埋下伏笔。

高潮来了,prototype本身也是一个object啊,所以它也有__proto__。然后就可以不断的往上链,直到Object.prototype停止,这个object具有一个特殊的值,即null。

4 忧之结

当我们面向类编程的时候,其实是在模块化,将数据和方法(函数)封装到一起。除此之外,继承和多态可以将多个模块关联到一起,实现了代码复用,一定程度上提升了可维护和可扩展性。

尽管JS没有类,但是也可以模块化,将数据和函数封装进函数。同时,原型链(__proto__prototype)可以将多个模块关联到一起,一样实现代码复用,一样能提升可维护和可扩展性。

只不过,JS把这内部操作暴露给了我们,初次见面总会有点小尴尬。现在,ES6来了,以后原型链的故事可就少有人说了。

参考