有对象吗?没有!?那我们 new 一个吧~

713 阅读8分钟

本文原创:tangyue

对于经常写JavaScript的小伙伴们,看到下面这段代码,想必不会陌生:

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.getName = function() {
    return this.name
![](https://user-gold-cdn.xitu.io/2019/11/8/16e4a5632658d2f1?w=794&h=450&f=png&s=114415)
}

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。

1.jpg

如果我们修改一下代码,给新对象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,用“类似类”的方式迷惑众人。

4.jpeg

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引擎来说,首字母是否大小写没有任何意义...

5.jpg

再看下面的代码:

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,看下它究竟何方妖孽

3.jpeg

从图片可以看出,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 还是会乖乖低头,举手投降,交出人质。

6.jpg

new 究竟做了啥

前面铺垫了这么多,回归主题,new 在函数调用时究竟做了什么呢?想必大家已经有些明白了,总结一下,使用new来调用函数时,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行[Prototype]](也就是__proto__)链接,指向用new调用的函数的prototype属性上
  3. 新对象会绑定到函数调用的this,并像调用普通函数那样,执行函数体里的代码
  4. 如果函数没有返回其他对象(例如:Object、Function、Date、Array、RegExp、Error),那么new表达式中的函数调用会自动返回这个新对象

小试牛刀时间~让我们手动实现一个 new

7.gif

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的内置属性。

2.jpeg

还有一个较为简单的实现方式:直接用 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),完美解决。

8.gif