原文:blog.xieyangogo.cn/2019/04/12/…javascript
相信不少才接触前端的小伙伴甚至工做几年的前端小伙伴对new这个操做符的了解还停留在只知其一;不知其二的地步,比较模糊。前端
就好比前不久接触到一个入职两年的前端小伙伴,他告诉我new是用来建立对象的,无可厚非,可能不少人都会这么答!java
那这么答究竟是错非常对呢?程序员
下面咱们全面来讨论一下这个问题:app
咱们要拿到一个对象,有不少方式,其中最多见的一种即是对象字面量:函数
var obj = {}
复制代码
可是从语法上来看,这就是一个赋值语句,是把对面字面量赋值给了
obj
这个变量(这样说或许不是很准确,其实这里是获得了一个对象的实例!!)ui不少时候,咱们说要建立一个对象,不少小伙伴双手一摸键盘,啪啪几下就敲出了这句代码。this
上面说了,这句话其实只是获得了一个对象的实例,那这句代码到底还能不能和建立对象画上等号呢?spa
咱们继续往下看。prototype
要拿到一个对象的实例,还有一种和对象字面量等价的作法就是构造函数:
var obj = new Object()
复制代码
这句代码一敲出来,相信小伙伴们对刚才我说的
obj
只是一个实例对象没有异议了吧!那不少小伙伴又会问了:这不就是
new
了一个新对象出来嘛!没错,这确实是new了一个新对象出来,由于javascript之中,万物解释对象。
obj是一个对象,并且是经过new运算符获得的,因此说不少小伙伴就确定的说:new就是用来建立对象的!
这就不难解释不少人把建立对象和实例化对象混为一谈!!
咱们在换个思路看看:既然js一切皆为对象,那为何还须要建立对象呢?自己就是对象,咱们何来建立一说?那咱们可不能够把这是一种
继承
呢?![]()
说了这么多,相信很多伙伴已经看晕了,可是咱们的目的就是一个:理清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)
console.log(person.name)
console.log(person.age)
console.log(person.gender)
console.log(person.nation)
person.say()
复制代码
call
或apply
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()
复制代码
执行代码,发现只有name
和gender
这两个字段如期输出,age
、nation
为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就是基本类型啊? ...
![]()
解惑: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()
复制代码
好了好了,小伙伴经常舒了一口气,这样总算完成了!! 可是,现实老是残酷的!
小伙伴:啥?还有完没完? ![]()
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()
报错,这又是为何呢?
从上图咱们能够清除地看到,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()
复制代码
这样建立的对象就拥有了它初始的原型链了,这个原型链是咱们传进来的构造函数赋予它的。
也就是说,咱们在建立新对象的时候,就为它指定了原型链了——新建立的对象继承自传进来的构造函数!
看到这里,小伙伴们长长舒了一口气,有本事你再给我安排一个坑出来!
![]()
既然都看到这里了,你们要相信咱们离最终的曙光已经不远了!
![]()
我想说的是,坑是没有了,可是为了程序员吹毛求疵的精神!哦不对,是精益求精的精神,咱们还有必要啰嗦一点点!!
想必细心的小伙伴已经注意到了,为何最后一步中的如下代码:
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操做符到底作了什么?
Constructor
;Constructor
的原型链结合Object.create
来建立
一个对象,此时新对象的原型链为Constructor
函数的原型对象;(结合咱们上面讨论的,要访问原型链上面的属性和方法,要使用实例对象的__proto__属性)Constructor
函数的this指向,指向新建立的实例对象,而后call
方法再调用Constructor
函数,为新对象赋予属性和方法;(结合咱们上面讨论的,要访问构造函数的属性和方法,要使用call或apply)Constructor
函数的一个实例对象。如今我,咱们来回答文章开始时提出的问题,new是用来建立对象的吗?
如今咱们能够勇敢的回答,new是用来作继承的,而建立对象的实际上是Object.create(null)。
在new操做符的做用下,咱们使用新建立的对象去继承了他的构造函数上的属性和方法、以及他的原型链上的属性和方法!
写在最后:
补充一点关于
原型链
的知识:
- JavaScript中的函数也是对象,并且对象除了使用字面量定之外,都须要经过函数来建立对象;
- prototype属性能够给函数和对象添加可共享(继承)的方法、属性,而__proto__是查找某函数或对象的原型链方式;
- prototype和__proto__都指向原型对象;
- 任意一个函数(包括构造函数)都有一个prototype属性,指向该函数的原型对象;
- 任意一个实例化的对象,都有一个__proto__属性,指向该实例化对象的构造函数的原型对象。
补充一下关于
Object.create()
的知识:
- Object.create(null)能够建立一个没有原型链、真正意义上的空对象,该对象不拥有js原生对象(Object)的任何特性和功能。 就如:即便经过人为赋值的方式(
newObj.__proto__ = constructor.prototype
)给这个对象赋予了原型链, 也不能实现原型链逐层查找属性的功能,由于这个对象看起来彷佛即便有了"__proto__"
属性,可是它始终没有直接或间接继承自Object.prototype, 也就不可能拥有js原生对象(Object)的特性或功能了;- 该API配合Object.defineProperty能够建立javascript极其灵活的自定义对象;
- 该API是实现继承的一种方式;
- ...