其它语言开发者初接触 Js 时,会对构造函数、new 关键字有些迷糊,因为这两个概念在其它 OO 语言中是很基本的概念,但在 Js 中用起来和在以前熟悉的语言中有些不一样,引起一些困惑。
问题
在 C++/Java/PHP 中都有 class 的概念,class 有构造函数以及通过 new 实例化对象看起来都很理所当然,没什么可疑惑的。
为什么会对 Js 中的 new
构造函数
就会产生一些疑惑呢,因为我们对这两个概念先入为主了。
我们先抛开一般 OO 语言中的 class 等这些语法、概念,从更 “原始” 的角度去理解这两个概念及其作用,理解起来就变得非常简单。
构造函数
构造函数
在 C++/java/PHP 中构造函数在形式上比较特殊,它们需要定义在一个 class 中,并且往往还有固定的函数名等。
抛开这些说,构造函数
首先是一个函数,这个函数基本作用是用来初始化一个实例对象。
在 Js 中,构造函数就是一个普通函数,它的定义也和普通的函数一样:
1 | function Person(name){ |
那么我们要怎么用这个函数 Person
来初始化一个对象呢?
这就要提到 执行环境
这个概念了,也就是 Person
函数中的 this
关键字,它就代表当前函数执行时的 执行环境
,在这里也就是我们要初始化的那个对象。
1 | let obj = {} |
这段代码就实现了——调用 Person
这个函数初始化了 obj
这个对象。
也就是说,我们通过 call
调用函数,第一个参数就是该函数的 执行环境
,也就是 Person
中的 this
。
但上面这段实例化对象的代码 “不好看”,不像在 Java/C++ 中直接使用 new
来的简洁,我们用以下代码来模拟实现一个类似 Java/C++ 的实例化语法:
- 指定一个构造函数及其参数
- 返回一个实例化的对象
1 | function New(constructor, ...args){ |
这个用法看起来就很 “像” Java/C++/PHP 中实例化一个对象的用法了。
当然,在 Js 中有同样简洁的方式创建和初始化一个实例:
1 | let obj2 = new Person('laogen2') |
到这里,你肯定就明白 new
一个构造函数到底是怎么回事了。你可以把 new
理解为一个语法糖,其实际工作就是我们上面模拟的 New
函数所做的。
也就是说 new
关键字首先创建了一个对象,然后调用 Person
函数初始化这个对象。
另外一个问题,我们直接调用构造函数 Person()
而不用 new
来调用会是什么样呢? Person()
里面的 this
到底指向什么对象呢?
1 | function Person(name){ |
我们直接调用 Person()
而不通过 new
调用,结果就是在当前执行环境中创建并初始化了 name
属性,所以直接打印 name
输出了 "laogen4"
。
也就是说,如果我们直接调用 Person()
,那么它的 this
就指向这个函数调用所在的执行环境,如果是在全局环境中调用的,那么这个执行环境: 在 Node.js 中就是 global
对象;在浏览器中通常是 window
对象。
this & call()
我们上面是通过调用 call
函数来完成的,那这个 call 又是怎么实现的呢?
这个 call
函数当然是 Js 本身就支持的,我们通过下面代码模拟实现它,以便更好的理解其原理,因为 this
是 Js 关键字,不可用做标识符,我们用 that
来模拟代替 this
:
1 | function Person(name){ |
通过这段代码可以看出, call()
把它的第一个参数以某种方式传给了被调用的函数 Person()
,并作用 Person()
函数的 this
(也就是代码中的 that
)。
这段代码只是模拟过程,实际上 this
对象并不是通过被调用函数的最后一个传数来传递的,这个是 Js 编译器层面去实现的,这里不细究。
bind()
1 | function Person(name){ |
这段代码模拟实现了 bind()
函数的逻辑,bind()
与 call()
的区别在于: bind()
并不调用 Person()
函数,它只是返回一个新函数,在这个新函数里调用了 call()
。
总结
这篇文主要说明了 Js 构造函数只是普通的函数,和其它 OO 语言不同的是,它并没有很特别的地方,构造函数的主要作用就是初始化实例对象;
通过模拟代码解释了 new
关键字的内部逻辑,以更好的理解构造函数的工作原理;
通过模拟 this
更进一步的理解 执行环境
的概念;
通过模拟 call()
和 bind()
方法,更直观深入的理解两者的逻辑和使用;