要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象;
1. new 操作符
在有上面的基础概念的介绍之后,在加上 new 操作符,我们就能完成传统面向对象的class + new的方式创建对象,在JavaScript中,我们将这类方式成为 Pseudoclassical。
基于上面的例子,我们执行如下代码
var obj = new Base();
这样代码的结果是什么,我们在Javascript引擎中看到的对象模型是:
new操作符具体干了什么呢? 其实很简单,就干了三件事情。
var obj = {}; obj.proto = Base.prototype; Base.call(obj); 第一行,我们创建了一个空对象obj; 第二行,我们将这个空对象的 __proto__ 成员指向了 Base 函数对象 prototype 成员对象; 第三行,我们将 Base 函数对象的 this 指针替换成obj,然后再调用 Base 函数,于是我们就给 obj 对象赋值了一个 id 成员变量, 这个成员变量的值是 ”base” ,关于 call 函数的用法。
如果我们给 Base.prototype 的对象添加一些函数会有什么效果呢?
Base.prototype.toString = function() { return this.id; }
那么当我们使用 new 创建一个新对象的时候,根据 proto 的特性,toString 这个方法也可以做新对象的方法被访问到。
于是我们看到了:
构造子对象中,我们来设置‘类’的成员变量(例如:例子中的id),构造子对象 prototype 中我们来设置 ‘类’ 的公共方法。于是通过函数对象和Javascript特有的 __ proto __ 与 prototype 成员及 new 操作符,模拟出类和类实例化的效果。
2. new操作中发生了什么
对于大部分前端开发者而言,new 一个构造函数或类得到对应实例,是非常普遍的操作了。
下面的例子中分别通过 构造函数 与 class类 实现了一个简单的创建实例的过程。
// ES5构造函数 let Dog = function (name, age) { this.name = name; this.age = age; }; Dog.prototype.sayName = function () { console.log(this.name); }; const myDog = new Dog('汪汪', 2); myDog.sayName() // '汪汪' // ES6 class类 class Cat { constructor(name, age) { this.name = name; this.age = age; } sayName() { console.log(this.name); } }; const myCat = new Cat('QIU', 3); myCat.sayName(); // QIU
但 new 不应该像一个黑盒,我们除了知道结果,更应该明白过程究竟如何。
比较直观的感觉,当我们 new 一个构造函数,得到的实例继承了构造器的构造属性( this.name 这些) 以及原型上的属性。
在《JavaScript模式》这本书中,new 的过程说的比较直白。当我们 new 一个构造器,主要有三步:
- 创建一个空对象,将它的引用赋给 this,继承函数的原型;
- 通过 this 将属性和方法添加至这个对象;
- 最后返回 this 指向的新对象,也就是实例(如果没有手动返回其他的对象);
改写上面的例子,大概就是这样:
// ES5构造函数 let Dog = function (name, age) { // 1.创建一个新对象,赋予this,这一步是隐性的, // let this = {}; // 2.给this指向的对象赋予构造属性 this.name = name; this.age = age; // 3.如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性的 // return this; }; const myDog = new Dog();
这应该不难理解,你应该在工作中看过类似下述代码中的操作,将this赋予一个新的变量(例如 that ),最后返回这个变量
// ES5构造函数 let Dog = function (name, age) { let that = this; that.name = name; that.age = age; return that; }; const myDog = new Dog('汪汪', 2);
为什么要这么写呢?
我在前面说 this 的创建与返回是隐性的,但在工作中为了让构造过程更易可见与更易维护,所以才有了上述使用 that 代替 this,同时手动返回 that 的做法;
这也验证了隐性的这两步确实是存在的。
但上述这个解释我觉得不够完美,它只描述了构造器属性是如何塞给实例,没说原型上的属性是如何给实例继承的。
我在winter大神的重学前端专栏中,看到了比较符合我心意的,同时也是符合原理的描述:
- 以构造器的 prototype 属性为原型,创建新对象;
- 将 this (也就是上一句中的新对象)和调用参数传给构造器,执行;
- 如果构造器没有手动返回对象,则返回第一步创建的对象;
到这里不管怎么说,你都应该大概知道了new过程中:会新建对象,此对象会继承构造器的原型与原型上的属性,最后它会被作为实例返回这样一个过程。知道了原理,我们来手动实现一个简单的new方法。
3. 实现一个简单的 new 方法
// 构造器函数 let Dog = function (name, age) { this.name = name; this.age = age; }; Dog.prototype.sayName = function () { console.log(this.name); }; // 定义的new方法 let newMethod = function (Dog, ...rest) { // 1. 以构造器的 prototype 属性为原型,创建新对象; let myDog = Object.create(Dog.prototype); // 2. 将this和调用参数传给构造器执行 Dog.apply(myDog, rest); // 3. 返回第一步的对象 return myDog; }; // 创建实例,将构造函数 Dog 与形参作为参数传入 const myDog = newMethod(Dog, '汪汪', 2); myDog.sayName() // '汪汪'; // 最后检验,与使用new的效果相同 console.log( myDog instanceof Dog ) // true console.log( myDog.hasOwnProperty('name') ); // true console.log( myDog.hasOwnProperty('age') ); // true console.log( myDog.hasOwnProperty('sayName') );// false