JS 的 new 做了那些事情

JS中的new运算符,从一个自定义对象类型或者包含constructor构建函数的内建对象类型中实例化一个对象。JS中已经“万物皆对象”。为什么还要存在实例化的操作呢?

首先,先看一段代码:
function Animal(name) {
  this.name = name
}
Animal.color = 'black'
Animal.say = function () {
  console.log('it is an ' + this.name)
}
Animal.prototype.say = function () {
  console.log('i am a ' + this.name)
}

var cat = new Animal('cat')
console.log(
  cat.name,  // cat
  cat.height  // undefined
)
cat.say()  // i am a cat
console.log(
  Animal.name, //Animal
  Animal.color //black
);
Animal.say(); //it is an Animal
  1. 首先定义了一个函数对象Animal。作为函数对象,Animal拥有原型对象prototype,prototype中拥有一个constructor函数,指向Animal函数自身。
  2. 给Animal函数对象添加了一个color属性,赋值为black
  3. 给Animal函数对象添加了一个say方法,该方法,读取当前调用的this对象中的name属性,打印字符串。
  4. 给Ainimal的原型对象添加一个say的方法,该方法,读取当前调用的this对象中的name属性,打印字符串。
  5. 通过new,从Animal函数对象中创建一个实例,将其赋值为变量cat
  6. 打印新建cat对象中的两个属性,name和height,分别打印为catundefined
  7. 调用cat对象的say方法,将cat对象作为this传递到Animal.prototype.say函数中,并执行打印输出
  8. 打印Animal函数对象中的两个属性,name和color,分别打印为Animalblack。其中的name属性是从Function.prototype上继承而来,color是Animal函数对象的自有属性。
  9. 调用Animal函数对象的say方法,将Animal函数对象作为this传递到Animal.say函数中,并执行打印输出

关于Animal,以及Animal的say,name属性的调用,都可以从函数对象的原型链的继承上得到解释。它的原型链是

Animal->Function.prototype->Object.prototype->null
我们重点关注下
var cat = new Animal('cat')

当JS中使用new操作符 添加到一个函数对象的前面并执行调用的时候。函数对象起到了一个自定义对象的constructor,也既构建函数的作用。
JS的new本身是一个“语法糖”,当JS解释器碰到new的时候,它会按照下面的伪代码执行:

// var cat = new Animal('cat')
var cat = (function () {
  let obj = {}
  obj.__proto__ = Animal.prototype
  let result = Animal.call(obj, 'cat')
  return (typeof result == 'object') ? result : obj
})()

将其转成规则,则new所起到的作用流程如下:

  1. 首先凭空创建一个空对象obj
  2. 把 obj 的proto 指向构造函数 Animal 的原型对象 prototype,此时便建立了 obj 对象的原型链:obj->Animal.prototype->Object.prototype->null
  3. 在 obj 对象的执行环境调用 Animal 函数并传递参数 “ cat ” 。 相当于 var result = obj.Animal("cat")。这句话,将this指向新创建的obj对象。并执行构建函数Animal。当这句执行完之后,obj 便产生了属性 name 并赋值为 "cat"。关于 call 的用法请参考:深入理解 call、apply 和 bind
  4. 考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 obj 作为新对象返回;否则会将 result 作为新对象返回。

此时cat的原型链是

cat -> Animal.prototype -> Object.prototype -> null
为什么要使用new来创建对象呢?

new的出现,让JS拥有了对象的继承能力,从例子中看到,通过new,成功在cat和Animal之间建立了继承的关系。cat可以调用Animal的原型对象上的方法。

通过 new 创建的 对象 和 构造函数 之间建立了一条原型链,原型链的建立,让原本孤立的对象有了依赖关系和继承能力,让JavaScript 对象能以更合适的方式来映射真实世界里的对象,这是面向对象的本质

测试下
function Foo(){
    getName = function(){
        console.log(1)
    }
    return this;
}
Foo.getName = function(){
    console.log(2)
}
Foo.prototype.getName = function(){
    console.log(3)
}
var getName = function(){
    console.log(4)
}
function getName(){
    console.log(5)
}
// ouput:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
  1. Foo.getName(): 调用Foo函数对象的getName方法,此时打印 2
  2. getName(); 调用全局的getName方法。JS代码执行分为两个阶段,具体参考执行上下文的文章,首先在代码为执行前,getName指的是打印 5 的函数声明,但在执行到此时的时候,上面 getName 全局变量的执行,将getName的赋值指向了 打印4 的函数。因此此时打印 4
  3. Foo().getName(); 将Foo函数执行后返回的对象作为this,并调用该this中包含的getName方法。首先,Foo()的调用是在全局作用域,因此return this 等价于 return window。Foo() == window。此时Foo().getName()变成了this.getName()。但此时并不等价于第二条,因为在Foo()执行的过程中,在Foo函数内部,对全局变量getName进行了重新赋值,此时全局函数getName打印输出 1
  4. getName(); 此时的结果和上一条打印结果输出相同,打印输出 1
  5. new Foo.getName(); 此时出现了new操作符,它将后面的函数Foo.getName作为了构建函数,创建了一个新的实例,在创建的过程中,会执行Foo.getName函数,因此此时输出 2
  6. new Foo().getName(); 此时出现了new操作符,根据就近原则,等价于(new Foo()).getName()。即先创建了Foo()对象的一个实例 obj = new Foo()。此时return的this等同于新建实例对象obj。接下来的调用变成了obj.getName,也既Foo.prototype.getName。打印输出 3
  7. new new Foo().getName(); 首先出现了两次new操作符,而每次new操作符,都需要跟随一个构建函数的调用。在表达式中一共有两次调用(函数的调用通过()实现)。因此按照就近原则,表达式等价于 new (new Foo()).getName()。new Foo(),根据new的执行原则,返回新建实例obj。此时等价于new obj.getName()。和第6条一样。因此此时,打印输出 3
最后打印顺序为 2,4,1,1,2,3,3

参考链接:
https://zhuanlan.zhihu.com/p/23987456
https://stackoverflow.com/questions/1646698/what-is-the-new-keyword-in-javascript
https://www.cnblogs.com/onepixel/p/5043523.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new

你可能感兴趣的:(JS 的 new 做了那些事情)