new 操作符到底做了什么?

原文:http://blog.xieyangogo.cn/2019/04/12/new-%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%88%B0%E5%BA%95%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F/


相信很多才接触前端的小伙伴甚至工作几年的前端小伙伴对new这个操作符的了解还停留在一知半解的地步,比较模糊。

就比如前不久接触到一个入职两年的前端小伙伴,他告诉我new是用来创建对象的,无可厚非,可能很多人都会这么答!

那这么答到底是错很是对呢?


下面我们全面来讨论一下这个问题:

我们要拿到一个对象,有很多方式,其中最常见的一种便是对象字面量:

var obj = {}

但是从语法上来看,这就是一个赋值语句,是把对面字面量赋值给了obj这个变量(这样说或许不是很准确,其实这里是得到了一个对象的实例!!)

很多时候,我们说要创建一个对象,很多小伙伴双手一摸键盘,啪啪几下就敲出了这句代码。

上面说了,这句话其实只是得到了一个对象的实例,那这句代码到底还能不能和创建对象画上等号呢?

我们继续往下看。

要拿到一个对象的实例,还有一种和对象字面量等价的做法就是构造函数:

var obj = new Object()

这句代码一敲出来,相信小伙伴们对刚才我说的obj只是一个实例对象没有异议了吧!

那很多小伙伴又会问了:这不就是new了一个新对象出来嘛!

没错,这确实是new了一个新对象出来,因为javascript之中,万物解释对象。

obj是一个对象,而且是通过new运算符得到的,所以说很多小伙伴就肯定的说:new就是用来创建对象的!

这就不难解释很多人把创建对象和实例化对象混为一谈!!

我们在换个思路看看:既然js一切皆为对象,那为什么还需要创建对象呢?本身就是对象,我们何来创建一说?那我们可不可以把这是一种继承呢?

image

说了这么多,相信不少伙伴已经看晕了,但是我们的目的就是一个:理清new是来做继承的而不是所谓的创建对象!!


那继承得到的实例对象有什么特点呢?

  1. 访问构造函数里面的属性
  2. 访问原型链上的属性

下面是一段经典的继承,通过这段代码来热热身,好戏马上开始:

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
}

Person.prototype.nation = '汉'

Person.prototype.say = function() {
  console.log(`My name is ${this.age}`)
}

var person = new Person('小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)

person.say()
  • 现在我们来解决第一个问题:我们可以通过什么方式实现访问到构造函数里面的属性呢?答案是callapply
function Parent() {
  this.name = ['A', 'B']
}

function Child() {
  Parent.call(this)
}

var child = new Child()
console.log(child.name) // ['A', 'B']

child.name.push('C')
console.log(child.name) // ['A', 'B', 'C']
  • 第一个问题解决了,那我们又来解决第二个:那又怎么访问原型链上的属性呢?答案是__proto__

现在我们把上面那段热身代码稍加改造,不使用new来创建实例:

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
}

Person.prototype.nation = '汉'

Person.prototype.say = function() {
  console.log(`My name is ${this.age}`)
}

// var person = new Person('小明', 25)
var person = New(Person, '小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)

person.say()

function New() {
  var obj = {}
  Constructor = [].shift.call(arguments) // 获取arguments第一个参数:构造函数
  // 注意:此时的arguments参数在shift()方法的截取后只剩下两个元素
  obj.__proto__ = Constructor.prototype // 把构造函数的原型赋值给obj对象
  Constructor.apply(obj, arguments) // 改变够着函数指针,指向obj,这是刚才上面说到的访问构造函数里面的属性和方法的方式
  return obj
}

以上代码中的New函数,就是new操作符的实现

主要步骤:

1. 创建一个空对象

2. 获取arguments第一个参数

3. 将构造函数的原型链赋给obj

4. 使用apply改变构造函数this指向,指向obj对象,其后,obj就可以访问到构造函数中的属性以及原型上的属性和方法了

5. 返回obj对象

可能很多小伙伴看到这里觉得new不就是做了这些事情吗,然而~~

然而我们却忽略了一点,js里面的函数是有返回值的,即使构造函数也不例外。

如果我们在构造函数里面返回一个对象或一个基本值,上面的New函数会怎样?

我们再来看一段代码:

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
  
  return {
    name: name,
    gender: '男'
  }
}

Person.prototype.nation = '汉'

Person.prototype.say = function() {
  console.log(`My name is ${this.age}`)
}

var person = new Person('小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)

person.say()

执行代码,发现只有namegender这两个字段如期输出,agenation为undefined,say()报错。

改一下代码构造函数的代码:

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
  
  // return {
  //   name: name,
  //   gender: '男'
  // }
  return 1
}

// ...

执行一下代码,发现所有字段终于如期输出。

这里做个小结:

1. 当构造函数返回引用类型时,构造里面的属性不能使用,只能使用返回的对象;

2. 当构造函数返回基本类型时,和没有返回值的情况相同,构造函数不受影响。

那我们现在来考虑下New函数要怎么改才能实现上面总结的两点功能呢?继续往下看:

function Person(name, age) {
  // ...
}

function New() {
  var obj = {}
  Constructor = [].shift.call(arguments)
  obj.__proto__ = Constructor.prototype
  
  // Constructor.apply(obj, arguments)
  var result = Constructor.apply(obj, arguments)
  
  // return obj
  return typeof result === 'object' ? result : obj
}

var person = New(Person, '小明', 25)

console.log(person.name)
// ...

执行此代码,发现已经实现了上面总结的两点。

解决方案:使用变量接收构造函数的返回值,然后在New函数里面判断一下返回值类型,根据不同类型返回不同的值。

看到这里。又有小伙伴说,这下new已经完全实现了吧?!!

答案肯定是否定的。

下面我们继续看一段代码:

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
  
  // 返回引用类型
  // return {
  //   name: name,
  //   gender: '男'
  // }
  
  // 返回基本类型
  // return 1
  
  // 例外
  return null
}

再执行代码,发现又出问题了!!!

又出问题了!为什么……?
...
刚才不是总结了返回基本类型时构造函数不受影响吗,而null就是基本类型啊?
...


image

解惑:null是基本类型没错,但是使用操作符typeof后我们不难发现:

typeof null === 'object' // true

特例:typeof null返回为'object',因为特殊值null被认为是一个空的对象引用

明白了这一点,那问题就好解决了:

function Person(name, age) {
  // ...
}

function New() {
  var obj = {}
  Constructor = [].shift.call(arguments)
  obj.__proto__ = Constructor.prototype
  // Constructor.apply(obj, arguments)
  var result = Constructor.apply(obj, arguments)
  // return obj
  // return typeof result === 'object' ? result : obj
  return typeof result === 'object' ? result || obj : obj
}

var person = New(Person, '小明', 25)

console.log(person.name)
// ...

解决方案:判断一下构造函数返回值result,如果result是一个引用(引用类型和null),就返回result,但如果此时result为false(null),就使用操作符||之后的obj

好了,到现在应该又有小伙伴发问了,这下New函数是彻彻底底实现了吧!!!

答案是,离完成不远了!!

在功能上,New函数基本完成了,但是在代码严谨度上,我们还需要做一点工作,继续往下看:

这里,我们在文章开篇做的铺垫要派上用场了:

var obj = {}

实际上等价于

var obj = new Object()

前面说了,以上两段代码其实只是获取了object对象的一个实例。再者,我们本来就是要实现new,但是我们在实现new的过程中却使用了new

这个问题把我们引入到了到底是先有鸡还是先有蛋的问题上!

这里,我们就要考虑到ECMAScript底层的API了——Object.create(null)

这句代码的意思才是真真切切地创建了一个对象!!

function Person(name, age) {
  // ...
}

function New() {
  // var obj = {}
  // var obj = new Object()
  var obj = Object.create(null)
  Constructor = [].shift.call(arguments)
  obj.__proto__ = Constructor.prototype
  // Constructor.apply(obj, arguments)
  var result = Constructor.apply(obj, arguments)
  // return obj
  // return typeof result === 'object' ? result : obj
  return typeof result === 'object' ? result || obj : obj
}

var person = New(Person, '小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
// 这样改了之后,以下两句先注释掉,原因后面再讨论
// console.log(person.nation)
// person.say()

好了好了,小伙伴常常舒了一口气,这样总算完成了!!
但是,现实总是残酷的!


image

小伙伴:啥?还有完没完?

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
}

Person.prototype.nation = '汉'

Person.prototype.say = function() {
  console.log(`My name is ${this.age}`)
}

function New() {
  // var obj = {}
  // var obj = new Object()
  var obj = Object.create(null)
  Constructor = [].shift.call(arguments)
  obj.__proto__ = Constructor.prototype
  // Constructor.apply(obj, arguments)
  var result = Constructor.apply(obj, arguments)
  // return obj
  // return typeof result === 'object' ? result : obj
  return typeof result === 'object' ? result || obj : obj
}

var person = New(Person, '小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
// 这里解开刚才的注释
console.log(person.nation)
person.say()

别急,我们执行一下修改后的代码,发现原型链上的属性nation和方法say()报错,这又是为什么呢?

image
image

从上图我们可以清除地看到,Object.create(null)创建的对象是没有原型链的,而后两个对象则是拥有__proto__属性,拥有原型链,这也证明了后两个对象是通过继承得来的。

那既然通过Object.create(null)创建的对象没有原型链(原型链断了),那我们在创建对象的时候把原型链加上不就行了,那怎么加呢?

function Person(name, age) {
  this.name = name
  this.age = age
  this.gender = '男'
}

Person.prototype.nation = '汉'

Person.prototype.say = function() {
  console.log(`My name is ${this.age}`)
}

function New() {
  Constructor = [].shift.call(arguments)
  
  // var obj = {}
  // var obj = new Object()
  // var obj = Object.create(null)
  var obj = Object.create(Constructor.prototype)
  
  // obj.__proto__ = Constructor.prototype
  // Constructor.apply(obj, arguments)
  var result = Constructor.apply(obj, arguments)
  // return obj
  // return typeof result === 'object' ? result : obj
  return typeof result === 'object' ? result || obj : obj
}

var person = New(Person, '小明', 25)

console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)

person.say()

这样创建的对象就拥有了它初始的原型链了,这个原型链是我们传进来的构造函数赋予它的。

也就是说,我们在创建新对象的时候,就为它指定了原型链了——新创建的对象继承自传进来的构造函数!

看到这里,小伙伴们长长舒了一口气,有本事你再给我安排一个坑出来!

image

既然都看到这里了,大家要相信我们离最终的曙光已经不远了!

image

我想说的是,坑是没有了,但是为了程序员吹毛求疵的精神!哦不对,是精益求精的精神,我们还有必要啰嗦一点点!!

想必细心的小伙伴已经注意到了,为什么最后一步中的以下代码:

Constructor = [].shift.call(arguments)
var obj = Object.create(Constructor.prototype)

不能使用以下代码来代替?

var obj = Object.create(null)
Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype

换个方式说,这两段代码大致的意思基本相同:都是将构造器的原型赋予新创建的对象。但是为何第二段代码要报错(访问不到原型链上的属性)呢?

这个问题很吃基本功,认真去研究研究js的底层APIObject.create以及原型链等知识,就会明白其中的道理。小伙伴可以拉到文章末尾,我把重点都记录下来了,以供大家参考。


现在,我们来梳理下最终的New函数做了什么事,也就是本文讨论的结果——new操作符到底做了什么?

  1. 获取实参中的第一个参数(构造函数),就是调用New函数传进来的第一个参数,暂时记为Constructor
  2. 使用Constructor的原型链结合Object.create创建一个对象,此时新对象的原型链为Constructor函数的原型对象;(结合我们上面讨论的,要访问原型链上面的属性和方法,要使用实例对象的proto属性)
  3. 改变Constructor函数的this指向,指向新创建的实例对象,然后call方法再调用Constructor函数,为新对象赋予属性和方法;(结合我们上面讨论的,要访问构造函数的属性和方法,要使用call或apply)
  4. 返回新创建的对象,为Constructor函数的一个实例对象。

现在我,我们来回答文章开始时提出的问题,new是用来创建对象的吗?

现在我们可以勇敢的回答,new是用来做继承的,而创建对象的其实是Object.create(null)。
在new操作符的作用下,我们使用新创建的对象去继承了他的构造函数上的属性和方法、以及他的原型链上的属性和方法!


写在最后:

补充一点关于原型链的知识:

  1. JavaScript中的函数也是对象,而且对象除了使用字面量定以外,都需要通过函数来创建对象;
  2. prototype属性可以给函数和对象添加可共享(继承)的方法、属性,而proto是查找某函数或对象的原型链方式;
  3. prototype和proto都指向原型对象;
  4. 任意一个函数(包括构造函数)都有一个prototype属性,指向该函数的原型对象;
  5. 任意一个实例化的对象,都有一个proto属性,指向该实例化对象的构造函数的原型对象。

补充一下关于Object.create()的知识:

  1. Object.create(null)可以创建一个没有原型链、真正意义上的空对象,该对象不拥有js原生对象(Object)的任何特性和功能。
    就如:即使通过人为赋值的方式(newObj.__proto__ = constructor.prototype)给这个对象赋予了原型链,
    也不能实现原型链逐层查找属性的功能,因为这个对象看起来似乎即使有了"__proto__"属性,但是它始终没有直接或间接继承自Object.prototype,
    也就不可能拥有js原生对象(Object)的特性或功能了;
  2. 该API配合Object.defineProperty可以创建javascript极其灵活的自定义对象;
  3. 该API是实现继承的一种方式;
  4. ...

原文:http://blog.xieyangogo.cn/2019/04/12/new-%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%88%B0%E5%BA%95%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容