资讯专栏INFORMATION COLUMN

JavaScript 中的 new 到底干了什么,跟原型链又有一些什么联系?

Cympros / 2495人阅读

摘要:原文如果按面向对象的思路去讲的,还是很难去理解,我们可以从另一个方向去理解一下它。

原文: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 () {}
  }
}
重复的资源占用

上面大家已经发现,像上面这样去创建三个对象, walkrunjumpspeakcoding 这五件能做的事情(方法),其实做法都一样,但是我们却重复的去描述该怎么做了,其实就占用了很多资源,所以,我们可能会像下面这样改进一下:

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

我们除了人,还有别的动物,比如 TigerFish等,按上面的方式,在 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 函数体中的自定义

返回新创建的对象

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103654.html

相关文章

  • 用愚公移山说明Javascript创建对象的各种姿势

    摘要:北山愚公者年且九十面山而居。工厂模式愚小公北山愚小小公北山工厂模式比较明显的一个缺点就是由于生成并返回了一个中间对象,所以不能判断对象的类型。   太行、王屋二山,方七百里,高万仞。本在冀州之南,河阳之北.......   嗯,按照惯例,第一句话就是骗你们点进来的。在读本文之前,希望你对Javascript的原型和原型链有一定了解,这有助于你更好的理解本文,之前有写过一篇相关文章,点此...

    cartoon 评论0 收藏0
  • 深入学习js之——原型原型

    摘要:我们用一张图表示构造函数和实例原型之间的关系好了构造函数和实例原型之间的关系我们已经梳理清楚了,那我们怎么表示实例与实例原型,也就是或者和之间的关系呢。 开篇: 在Brendan Eich大神为JavaScript设计面向对象系统的时候,借鉴了Self 和Smalltalk这两门基于原型的语言,之所以选择基于原型的面向对象系统,并不是因为时间匆忙,它设计起来相对简单,而是因为从一开始B...

    FingerLiu 评论0 收藏0
  • 深入学习js之——原型原型

    摘要:我们用一张图表示构造函数和实例原型之间的关系好了构造函数和实例原型之间的关系我们已经梳理清楚了,那我们怎么表示实例与实例原型,也就是或者和之间的关系呢。 开篇: 在Brendan Eich大神为JavaScript设计面向对象系统的时候,借鉴了Self 和Smalltalk这两门基于原型的语言,之所以选择基于原型的面向对象系统,并不是因为时间匆忙,它设计起来相对简单,而是因为从一开始B...

    xialong 评论0 收藏0
  • 前端基础进阶(九):详解面向对象、构造函数、原型原型

    摘要:我们通过一个简单的例子与图示,来了解构造函数,实例与原型三者之间的关系。而原型对象的指向构造函数。于是根据构造函数与原型的特性,我们就可以将在构造函数中,通过声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。 showImg(https://segmentfault.com/img/remote/1460000008593382); 如果要我总结一下学习前端以来我遇...

    Tony_Zby 评论0 收藏0
  • 进击的 JavaScript (七)之 原型

    摘要:创建一个新的对象即实例对象把新对象的指向后面构造函数的原型对象。简单来验证一下等同与对象没有原型对象的原型对像等同于构造函数是等同于,构造函数是七原型链的作用其实,原型链的根本作用就是为了属性的读取。 首先说一下,函数创建的相关知识 在JavaScript中,我们创建一个函数A(就是声明一个函数), 那么 js引擎 就会用构造函数Function来创建这个函数。所以,所有的函数的con...

    ivydom 评论0 收藏0

发表评论

0条评论

Cympros

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<