本文原创:tangyue
对于经常写JavaScript的小伙伴们,看到下面这段代码,想必不会陌生:
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.getName = function() {
return this.name
}
let person1 = new Person('Tom', 18)
let person2 = new Person('June', 20)
person1.name // Tom
person1.age // 18
person.getName() // Tom
person2.name // June
person2.age // 20
person2.getName() // June
我们在调用 new
时,会创建两个新的对象 person1 和 person2,即使我们并没有手动给 person1 和 person2 分别添加 name、age 属性和 getName 方法,但 person1 和 person2 通过神奇的 new
操作后,都拥有了各自的 name,age 属性以及 getName 方法。那这个 new
都在幕后帮我们做了些什么呢?先别急,在解密前先带大家梳理几个重要的知识点。
原型链
JavaScript中有一个特殊的内置属性[[Prototype]]
,所有对象在创建后都会有一个非空的[[Prototype]]
属性,当我们访问对象某个属性时,会触发[[get]]
操作,它首先会检查对象本身是否有要访问的属性,有就直接使用它,没有的话,就会访问对象的[[Prototype]]
链,顺着完整的原型链继续寻找匹配的属性。
所以当调用新创建的 person 对象的 getName 方法时,[[get]]
会顺着[[Prototype]]
链查找,在 Person.prototype中找到,直接调用。绝大多数的浏览器支持一种非标准的方法来访问内置的[[Prototype]]
属性:__proto__
,__proto__
指向的是创建这个对象的 prototype
person.__proto__ === Person.prototype // true, person 由 Person 创建,所以 __proto__ 指向 Person.prototype
Person.__proto__ === Function.prototype // true, Person 是函数,本质是由Function 创建:var Person = new Function('name', 'age', 'this.name = name; this.age = age'),__proto__ 指向 Function.prototype
Person.prototype.__proto__ === Object.prototype //true, __proto__终点指向Object.prototype
我们在控制台打印出person对象,可以看到,顺着person对象的原型链__proto__
,一直向上层查找,便能访问到Person对象的原型方法getName,以及Object内置属性toString,valueOf等。所有对象的[[Prototype]]
链最终都会指向内置的Object.prototype。
如果我们修改一下代码,给新对象person1添加一个属性getName
person1.getName = function() {
return 'I am ' + this.name
}
person1.getName() // I am Tom
person2.getName() // June
此时的 person1 调用 getName 结果和 person2 调用结果不一样了,原因是 person1 有了自己的 getName 方法,不需要和 person2 一样沿着[[Prototype]]
原型链向上层查找了。如果属性 getName 既出现在 person1 中,也出现在 person1 的 [Prototype]]
链上层,就会发生属性的屏蔽。person1 会屏蔽原型链上层的同名属性。
说到这,大家是不是觉得这个跟“类”、“继承”、“实例”、“构造函数”,“多态”等面向类语言有点相似?多年以来,JavaScript一直在做一件无耻的事,模仿类,但JavaScript和真正面向类的语言不同,它并没有类来作为对象的抽象模式,JavaScript 只有对象。JavaScript 狡猾的利用了函数这个特殊的公有且不可枚举的属性prototype
,用“类似类”的方式迷惑众人。
prototype
属性指向一个对象,也就是原型对象,我们可以通过obj.prototype
的方式访问 obj 的原型对象。
在JavaScript中,不能创建一个类的多个实例,只能创建多个对象,这些对象通过[Prototype]]
关联同一个对象,并且创建新的对象时不会复制属性,而是通过原型链接的方式访问另一个对象的属性和方法。
当我们用new
调用函数Person()
时,实质就是让新创建的每个对象的原型链[Prototype]]
链接到Person.prototype
这个对象上。
可以验证下:
function Person(name, age) {
this.name = name
this.age = age
}
var person = new Person('Karen', 12)
Object.getPrototypeOf(person) === Person.prototype // true
构造函数
JavaScript还用了别的方式来“伪装”自己,就是“构造函数”。这里为什么会打上引号呢?原因是在JavaScript中,其实并没有真正意义上的构造函数,这些长得像构造函数的函数,实际上就是一个普通的函数。再回顾下代码:
function Person() {
// do sth.
}
var person = new Person()
我们用了关键字new
来调用了Person,看起来像是初始化类时执行了类的构造函数方法。按照惯例,函数Person首字母大写,更加造成了类的假象,但对于JavaScript引擎来说,首字母是否大小写没有任何意义...
再看下面的代码:
function Person(name) {
this.name = name
console.log('I am ' + this.name)
}
// 普通函数调用
Person('Jim') // I am Jim
// new 关键字调用
var person = new Person('Tom') // I am Tom
console.log(person) // Person {name: "Tom"}
在普通的函数调用前加上new
关键字之后,就会把这个普通的函数调用变得不普通,new
会劫持普通函数并用构造对象的形式来调用它:执行完Person函数后,会构造一个[Prototype]]
已链接到Person.prototype的对象,并赋值给person。像实例中的Person函数一样,它们并不是构造函数,当且仅当使用关键字new
调用后,函数调用会变成“构造函数调用”。
再思考一下,如果这个“构造函数”执行后,函数有自己的return
,那会不会影响new
出的新的对象呢?我们来实验一下~
function Person(name) {
this.name = name
console.log('I am ' + this.name)
return 'Hello ' + this.name
}
// 先把Person当做普通函数调用
var person1 = Person('Tom') // I am Tom
console.log(person1) // Hello Tom
// 把Person当做构造函数调用
var person2 = new Person('Jim') // I am Jim
console.log(person2) // Person {name: "Jim"}
哈哈看出区别没?当我们用关键字new
来调用 Person() 时,函数Person自身的return
: 'Hello ' + this.name 并没有赋值给person2,person2 如愿以偿的成为一个拥有属性name,且[Prototype]]
链接到Person.prototype 新对象。此时我们是不是可以这样想,强大的new
劫持了普通函数,让函数本身的return
失效?先别这么早下结论,我们再继续看:
function Person(name) {
this.name = name
console.log('I am ' + this.name)
let obj = {
name: this.name,
age: 18
}
return obj
}
Person.prototype.getName = function() {
console.log('Hi' + this.name)
}
Person.prototype.age = 20
// 先把Person当做普通函数调用
var person1 = Person('Tom') // I am Tom
console.log(person1) // {name: "Tom", age: 18}
// 把Person当做构造函数调用
var person2 = new Person('Jim') // I am Jim
console.log(person2) // {name: "Jim", age: 18}
惊人的一幕出现了,person2 和 person1 一样,都成为Person普通调用后的return值,看起来new
劫持失败了。
在控制台打印出person2,看下它究竟何方妖孽
从图片可以看出,person2 就是一个普通的 {name: "Jim", age: 18} 对象,属性值由Person return的对象决定。并且person2的原型链[Prototype]]
已和 Person.prototype
失联,__proto__
直接链接到Object.prototype
,当我们尝试调用person2.getName()
,顺着原型链向上查找,没有找到getName的函数,结果控制台报了Uncaught TypeError的错误
这么一看,new
的函数调用劫持也没有想象中那么强悍啊,当遇到的函数有自身的return值,且return的不是基本数据类型(Number, Boolean, String, Null, Undefined)时,new
还是会乖乖低头,举手投降,交出人质。
new 究竟做了啥
前面铺垫了这么多,回归主题,new
在函数调用时究竟做了什么呢?想必大家已经有些明白了,总结一下,使用new
来调用函数时,会自动执行下面的操作:
- 创建一个全新的对象
- 这个新对象会被执行
[Prototype]]
(也就是__proto__
)链接,指向用new
调用的函数的prototype属性上- 新对象会绑定到函数调用的this,并像调用普通函数那样,执行函数体里的代码
- 如果函数没有返回其他对象(例如:Object、Function、Date、Array、RegExp、Error),那么
new
表达式中的函数调用会自动返回这个新对象
小试牛刀时间~让我们手动实现一个 new
吧
function _new(fn, ...args) {
var obj = {} // 步骤一:创建一个新对象
obj.__proto__ = fn.prototype // 步骤二:执行[Prototype]]链接
var res = fn.call(obj, ...args) // 步骤三:绑定this,执行函数
return typeof res == 'object' ? res : obj // 步骤四:返回对象
}
function Person(name, age) {
this.name = name
this.age = age
this.a = 'a'
this.b = function() {
console.log('b')
}
}
Person.prototype.c = function() {
console.log('c')
}
Person.prototype.d = 'd'
var person1 = _new(Person, 'Tom', 18)
console.log(person1)
浏览器控制台打印的结果为下图:我们成功通过_new函数创建了一个新的对象person1,person1 有自己的属性name, age, a, b, 通过[Prototype]]
链接可以在访问到上层Person.prototype对象的属性c, d, 通过原型链再向上,可以访问到Object.prototype的内置属性。
还有一个较为简单的实现方式:直接用 Object.create(fn.prototype)
就能一次性完成步骤一、二
function _new2(fn, ...args) {
var obj = Object.create(fn.prototype) // 步骤一:创建一个新对象,步骤二:执行[Prototype]]链接
var res = fn.call(obj, ...args) // 步骤三:绑定this,执行函数
return typeof res == 'object' ? res : obj // 步骤四:返回对象
}
在最后补充一下如此优秀的Object.create()
函数原理,Object.create()
会创建一个对象,并把这个对象的[Prototype]]
关联到指定的对象。我们正好需要创建一个新的对象,并且关联新对象的[Prototype]]
到函数的prototype
,于是直接调用 Object.create(fn.prototype)
,完美解决。