我们先来思考一下下面代码的输出,来引入今天的问题:
const a = new Foo()
console.log(a.id) // 1
const b = Foo()
console.log(b.id) // 2
const c = new Foo()
console.log(c.id) // 3
id
是函数对象Foo
中的成员属性,每一次对Foo
函数的调用,得到一个Foo
对象的实例,打印id
值,引出两个问题:
- 每个实例对象的id值每次调用都是递增的,是怎么实现的呢?
new Foo()
和Foo()
调用的区别是什么呢?
本着能动手就先动手实现的原则,后面再分析原理,我先写代码实现一波先:
第一版
let tmp = 1
function Foo() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
我们实现了上图展示的效果,但是这里用到了全局变量tmp
,我们都知道全局变量会透着一股不好的味道,会引出意想不到的bug出来,所以才会出现诸如模块化机制
来隔离变量作用域等;所以,我们打算第二版引入闭包来优化一下代码,如下所示:
第二版
let Foo = (function() {
let tmp = 1
return function() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
})()
这里我们是通过 立即执行函数 的方式去实现的,tmp
封装在立即执行函数的内部;现在,在Foo
函数的外部是访问不到tmp
变量的,并且返回一个函数作为返回值,里面的这个内部函数我们就叫做 闭包 了,闭包就可以访问到外部函数的tmp
变量。
相信读者读到这里,关于第一个问题的解答应该是解释清楚了:本质就是通过闭包(内部函数)调用一个内部变量递增实现给id
属性赋值。
接下来我们来解答一下第二个问题:new
运算符加与不加有何区别?
直接引入MDN上面的解释吧:
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
1.创建一个空的简单JavaScript对象(即{});
2.链接该对象(即设置该对象的构造函数)到另一个对象 ;
3.将步骤1新创建的对象作为this的上下文 ;
4.如果该函数没有返回对象,则返回this。
我们先来看一下下面这一段代码
function Foo() {
this.id = 1
}
const f = Foo()
console.log(f.id) // TypeError: Cannot read property 'id' of undefined
这里没有加 new
运算符, 仅仅是普通的 foo()
函数调用,this
的指向是根据运行时决定的,很明显 this
目前所在的环境是 全局上下文对象
,在浏览器环境下,foo()
的调用就等价于 window.foo()
的调用,因此, this
的指向就是 window
本身啦,this.id = 1
的操作本质上就相当于window.id = 1
console.log(window.id) // 1
说了一大堆,接下来正式进入主题,加上new
运算符之后实际上就是改变了函数对象内部this
的指向了,这时候this
指向了该函数对象创建的实例本身了。
function Foo() {
this.id = 1
}
const f = new Foo()
console.log(f.id) // 1
再返回来看一下刚开始的问题:
const b = Foo()
console.log(b.id) // 2
这个 b
实例看上去只是普通的函数对象调用,反而能够使用了内部的id属性,这不是很奇怪吗?我们再去看里面的具体函数实现:
let Foo = (function() {
let tmp = 1
return function() {
if (this instanceof Foo) {
this.id = tmp++
} else {
return new Foo()
}
}
})()
原来内部使用了 instanceof
运算符去判断了一下 当前的 this
是否为 Foo
创建的实例,否则会再次通过 new
去调用一次自身去获得一个实例对象,这样就能正确的使用到它的id
属性啦。
结论: 在 js
里面,我们所说的 构造函数 无非其实就是在 普通函数 前面加上一个 new
运算符,以此来获得函数对象的属性,本质上还是this
的指向问题。所以大家不要被迷惑了。
关于 new
的小结暂且告一段落,后期想到再做补充~