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
- 首先定义了一个函数对象Animal。作为函数对象,Animal拥有原型对象prototype,prototype中拥有一个constructor函数,指向Animal函数自身。
- 给Animal函数对象添加了一个color属性,赋值为black
- 给Animal函数对象添加了一个say方法,该方法,读取当前调用的this对象中的name属性,打印字符串。
- 给Ainimal的原型对象添加一个say的方法,该方法,读取当前调用的this对象中的name属性,打印字符串。
- 通过new,从Animal函数对象中创建一个实例,将其赋值为变量cat
- 打印新建cat对象中的两个属性,name和height,分别打印为
cat
和undefined
- 调用cat对象的say方法,将cat对象作为this传递到Animal.prototype.say函数中,并执行打印输出
- 打印Animal函数对象中的两个属性,name和color,分别打印为
Animal
和black
。其中的name属性是从Function.prototype上继承而来,color是Animal函数对象的自有属性。- 调用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所起到的作用流程如下:
- 首先凭空创建一个空对象obj
- 把 obj 的proto 指向构造函数 Animal 的原型对象 prototype,此时便建立了 obj 对象的原型链:obj->Animal.prototype->Object.prototype->null
- 在 obj 对象的执行环境调用 Animal 函数并传递参数 “ cat ” 。 相当于 var result = obj.Animal("cat")。这句话,将this指向新创建的obj对象。并执行构建函数Animal。当这句执行完之后,obj 便产生了属性 name 并赋值为 "cat"。关于 call 的用法请参考:深入理解 call、apply 和 bind
- 考察第 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();
- Foo.getName(): 调用Foo函数对象的getName方法,此时打印 2
- getName(); 调用全局的getName方法。JS代码执行分为两个阶段,具体参考执行上下文的文章,首先在代码为执行前,getName指的是打印 5 的函数声明,但在执行到此时的时候,上面 getName 全局变量的执行,将getName的赋值指向了 打印4 的函数。因此此时打印 4
- Foo().getName(); 将Foo函数执行后返回的对象作为this,并调用该this中包含的getName方法。首先,Foo()的调用是在全局作用域,因此return this 等价于 return window。Foo() == window。此时Foo().getName()变成了this.getName()。但此时并不等价于第二条,因为在Foo()执行的过程中,在Foo函数内部,对全局变量getName进行了重新赋值,此时全局函数getName打印输出 1
- getName(); 此时的结果和上一条打印结果输出相同,打印输出 1
- new Foo.getName(); 此时出现了new操作符,它将后面的函数Foo.getName作为了构建函数,创建了一个新的实例,在创建的过程中,会执行Foo.getName函数,因此此时输出 2
- new Foo().getName(); 此时出现了new操作符,根据就近原则,等价于(new Foo()).getName()。即先创建了Foo()对象的一个实例 obj = new Foo()。此时return的this等同于新建实例对象obj。接下来的调用变成了obj.getName,也既Foo.prototype.getName。打印输出 3
- 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