原文:https://legacy.ofcrab.com/press/javascript-new.html
如果按面向对象的思路去讲 JavaScript 的 new
,还是很难去理解,我们可以从另一个方向去理解一下它。
你这些人类
我是一名程序员,也是一个人,我可能:
- 有一个响亮亮的名称
- 在某一天出生
- 是个男人
- 我能行走
- 我还能跑步
- 还能跳跃
- 能说话
- 我还能写代码
那么,在 JavaScript 中,我们可能像下面这样表达我:
const me = {
name: '大胡子农同工潘半仙',
birth: '1988-08-08',
sex: 'male',
walk: function (speed, direction, duration) {
// 以 speed 的速度向 direction 方向行走 duration 长的时间
},
run: function (speed, direction, duration) {
// 像跑步一样,速度
},
jump: function (high, direction, angle) {
// 以 angle 角度向 direction 方向跳 high 高
},
speak: function (letters) {
// 说出 letters 这些词
},
coding: function (language, line) {
// 写程序呢
}
}
你们这些人类
当然,这个世界上不可能只有我一个程序员,更不可能只有我一个人,就像我们这个小公司,就有七八百人,似乎所有这些人的数据都保存在数据库里面:
name | sex | birth |
---|---|---|
潘韬 | male | 1988-08-08 |
高超 | male | 1985-08-09 |
春雨 | male | 1999-08-08 |
我们从数据库中查询出上面这几条记录,在 JavaScript 可能表示为一个二维数据,然后要创建出这三个人来,可能是下面这样的:
const people = DB.query()
// people = [['潘韬', 'male', '1988-08-08'], [...], [...]]
for (let i = 0; i < people.length; i++) {
let [name, sex, birth] = people[i]
people[i] = {
name,
sex,
birth,
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
}
重复的资源占用
上面大家已经发现,像上面这样去创建三个对象, walk
、run
、jump
、speak
、coding
这五件能做的事情(方法),其实做法都一样,但是我们却重复的去描述该怎么做了,其实就占用了很多资源,所以,我们可能会像下面这样改进一下:
const walk = function walk () {}
const run = function run () {}
const jump = function jump () {}
const speak = function speak () {}
const coding = function coding () {}
for (let i = 0; i < people.length; i++) {
let [name, sex, birth] = people[i]
people[i] = {
name,
sex,
birth,
walk,
run,
jump,
speak,
coding
}
}
不同的人共用相同的资源(方法)
但是这个世界不止有人类
对,人类相比于这个世界上的其它生物来讲,数量根本就值得一提,如果像上面这样,可能各种不同物种能做的事情都会要定义出不同的函数,蠕动肯定不是人类会去做的事情,但很多别的生物会做,那么为了代码管理方便,我们把人能做的所有事情都放在一个对象里面,这样就相当于有了一个命名空间了,不会再跟别的物种相冲突:
const whatPeopleCanDo = {
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
for (let i = 0; i < people.length; i++) {
let [name, sex, birth] = people[i]
people[i] = {
name,
sex,
birth,
...whatPeopleCanDo
}
}
原型
但是,有的人可能我们并不知道他的 sex
信息是多少,有的也有可能不知道 birth
是多少,但是我们希望在创建这个人的时候,能给不知道的数据一些初始数据,所以, whatPeopleCanDo
并不能完全的表达出一个人,我们再改进:
const peopleLike = {
name: '',
sex: 'unknown',
birth: '',
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
for (let i = 0; i < people.length; i++) {
let [name, sex, birth] = people[i]
people[i] = {
...peopleLike,
name: name || peopleLike.name,
sex: sex || peopleLike.sex,
birth: birth || peopleLike.birth
}
}
这样一来,我们就可以为不知道的属性加一些默认值,我们称 peopleLike
这个东东就为原型,它表示了像人类这样的物种有哪些属性,能干什么事情。
原型链
虽然上面已经比最开始的版本好得多了,但是还是能有很大的改进空间,我们现在像下面这样改一下:
const peoplePrototype = {
name: '',
sex: 'unknown',
birth: '',
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
for (let i = 0; i < people.length; i++) {
let [name, sex, birth] = people[i]
people[i] = {
name: name || peoplePrototype.name,
sex: sex || peoplePrototype.sex,
birth: birth || peoplePrototype.birth,
__proto__: peoplePrototype
}
}
我们不再把人类原型里面的所有方法都绑定到某个人身上,而是像上面这样,用一个特殊的字段 __proto__
来指定:我的原型是 peoplePrototype
这个对象,同时,我们还制定了一个规则:如果你想请求我的某个方法,在我自己身上没有,那就去我的原型上面找吧,如果我的原型上面没有,那就去我的原型的原型上面去找,直到某个位置,没有更上层的原型为止
像上面这样创建的 people
对象,有自己的属性,但是当我们去访问 people.speak()
方法的时候,其实访问的是 people.__proto__.speak()
,这是我们的规则。
更优雅的创建新新人类
我们总不能在需要创建新人的时候,都像上面这样,自己去写一个对象,然后再手工指定它的原型是什么,所以,我们可以创建一个函数,专门用来生成人类的:
const peoplePrototype = {
name: '',
sex: 'unknown',
birth: '',
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
const makePeople = function makePeople(name, sex, birth) {
let people = {}
people.name = name || peoplePrototype.name
people.sex = sex || peoplePrototype.sex
people.birth = birth || peoplePrototype.birth
people.__proto__ = peoplePrototype
return people
}
people = people.map(makePeople)
现在这样我们只需要引入 makePeople
这个函数就可以随时随地创建新人了。
更优雅一点的改进
显然,上面这样并不是最好的办法,定义了一个原型,又定义了一个原型对象,我们可以把这两个合并到一起,所以,就可以有下面这样的实现了:
const People = function People (name, sex, birth) {
let people = {}
people.name = name || People.prototype.name
people.sex = sex || People.prototype.sex
people.birth = birth || People.prototype.birth
people.__proto__ = People.prototype
return people
}
People.prototype = {
name: '',
sex: 'unknown',
birth: '',
walk: function () {},
run: function () {},
jump: function () {},
speak: function () {},
coding: function () {}
}
我们直接把创建人类的那个函数叫作 People
,这个函数有一个属性叫 prototype
,它表示用我这个函数创建的对象的原型是什么,这个函数做的事情还是以前那些事儿,创建临时对象,设置对象的属性,绑定一下原型,然后返回。
神奇的 this
我们除了人,还有别的动物,比如 Tiger
、Fish
等,按上面的方式,在 Tiger()
或者 Fish()
函数里面都会建立不同的 tiger
或者 fish
名称的临时对象,这样太麻烦,我们把这种函数创建出来的对象,都可以统一叫作“这个对象” ,也就是 this object
,不在关心是人是鬼,统一把所有的临时对象都叫 thisObject
或者更简单的就叫作:这个,即 this
。
const People = function People (name, sex, birth) {
let this = {}
this.name = name || People.prototype.name
this.sex = sex || People.prototype.sex
this.birth = birth || People.prototype.birth
this.__proto__ = People.prototype
return this
}
当然,上面的这一段代码是有问题的,只是假想一样,这样是不是可行。
new
到现在为止,我们发现了整个代码的演变,是时候引出这个 new
了,它来干什么呢?它后面接一个类似上面这种 People
的函数,表示我需要创建一个 People
的实例,它的发明就是为了解决上面这些所有重复的事情,有了 new
之后,我们不需要再每一次定义一个临时对象,在 new
的上下文关系中,会在 People
函数体内自动为创建一个临时变量 this
,这个就表示即将被创建出来的对象。同时,对于使用 new
创建的实例,会自动的绑定到创建函数的 prototype
作为原型,还会自动为 People
创建一个 constructor
函数,表示这个原型的创建函数是什么,所以,我们可以改成下面这样的了:
const People = function People (name, sex, birth) {
this.name = name || People.prototype.name
this.sex = sex || People.prototype.sex
this.birth = birth || People.prototype.birth
}
People.prototype.name = ''
People.prototype.sex = 'unknown'
People.prototype.birth = ''
People.prototype.walk = function () {}
People.prototype.run = function () {}
People.prototype.jump = function () {}
People.prototype.speak = function () {}
People.prototype.coding = function () {}
people = people.map(p => new People(...p))
总结
new
到底干了什么?当 new People()
的时候
- 创建临时变量
this
,并将this
绑定到People
函数体里 - 执行
People.prototype.constructor = People
- 执行
this.__proto__ = People.prototype
- 执行
People 函数体中的自定义
- 返回新创建的对象