共计 7377 个字符,预计需要花费 19 分钟才能阅读完成。
原文:http://blog.xieyangogo.cn/201…
相信很多才接触前端的小伙伴甚至工作几年的前端小伙伴对 new 这个操作符的了解还停留在一知半解的地步,比较模糊。
就比如前不久接触到一个入职两年的前端小伙伴,他告诉我 new 是用来创建对象的,无可厚非,可能很多人都会这么答!
那这么答到底是错很是对呢?
下面我们全面来讨论一下这个问题:
我们要拿到一个对象,有很多方式,其中最常见的一种便是对象字面量:
var obj = {}
但是从语法上来看,这就是一个赋值语句,是把对面字面量赋值给了 obj 这个变量(这样说或许不是很准确,其实这里是得到了一个对象的实例!!)
很多时候,我们说要创建一个对象,很多小伙伴双手一摸键盘,啪啪几下就敲出了这句代码。
上面说了,这句话其实只是得到了一个对象的实例,那这句代码到底还能不能和创建对象画上等号呢?我们继续往下看。
要拿到一个对象的实例,还有一种和对象字面量等价的做法就是构造函数:
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 操作符的实现
主要步骤:
创建一个空对象
获取 arguments 第一个参数
将构造函数的原型链赋给 obj
使用 apply 改变构造函数 this 指向,指向 obj 对象,其后,obj 就可以访问到构造函数中的属性以及原型上的属性和方法了
返回 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
}
// …
执行一下代码,发现所有字段终于如期输出。
这里做个小结:
当构造函数返回引用类型时,构造里面的属性不能使用,只能使用返回的对象;
当构造函数返回基本类型时,和没有返回值的情况相同,构造函数不受影响。
那我们现在来考虑下 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()
这样创建的对象就拥有了它初始的原型链了,这个原型链是我们传进来的构造函数赋予它的。
也就是说,我们在创建新对象的时候,就为它指定了原型链了,新创建的对象继承自传进来的构造函数!
现在,我们来梳理下最终的 New 函数做了什么事,也就是本文讨论的结果————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__属性,指向构造函数的原型对象。
原文:http://blog.xieyangogo.cn/201…