new的过程
借用 MDN 对 new 的说明
new 运算符创建一个用户定义的对象类型(当构造函数有返回值时)的实例或具有构造函数的内置对象(当前函数可用来作为构造函数,那么返回内部创建的新对象)的实例
new 一个对象的过程分为四步
- 创建一个新对象
- 将构造函数的作用域赋值给新对象 (this指向这个新对象)
- 执行构造函数中的代码
- 返回新对象
一个普通的构造函数
function Person(name) {
this.name = name
}
var p = new Person('小明')
console.log(p.name) // 小明
console.log( p instanceof Person ) // true
用js 模拟一个new 的函数
function _new () {
// 1. 创建一个对象
let target = {}
let [constructor,...args] = [...arguments]; // 第一个参数是构造函数
// 2. 原型链连接
target.__proto__ = constructor.prototype
// 3. 将构造函数的属性和方法添加到这个新的空对象上
let result = constructor.apply(target,args)
if(result && (typeof result == 'object' || typeof result == 'function')){
// 如果构造函数返回的结果是一个对象,就返回这个对象
return result
}
// 如果构造函数返回的不是一个对象,就返回创建的新对象。
return target
}
let p2 = _new(Person, "小花")
console.log(p2.name) // 小花
console.log(p2 instanceof Person ) // true
上半部分的原文连接:https://www.jianshu.com/p/9bf81eea03b2
关于如何判断函数是否可以作为构造函数
我们通过function定义的普通函数都可以结合 new 来作为构造函数使用,那么到底如何判断一个函数能否作为构造函数呢
根据网上的文章描述
每个函数都有一些内部的属性,例如
Construct
表示可以作为构造函数使用,Call
表示可以用来作为普通函数
所以当一个函数没有Construct
内部属性时,它就不能用来作为构造函数
1. 经验积累方式
- 箭头函数不能作为构造函数使用
- Generator函数不能作为构造函数使用
- 对象的简写方法不能作为构造函数使用
- 内置方法不能作为构造函数使用(如Math.min)
除非特别说明,es6+ 实现的特定函数都没有实现[constructor] 内置方法
除了依靠经验,还有另类思路,如下
- 通过构造函数是否有该属性判断 Fn.prototype.constructor, 但有局限性,无法处理手动修改的场景
- 通过抛出异常,局限性是依赖原有new操作符,而且会导致构造函数逻辑被先行处理
- 通过 Reflect.construct , 加上 Symbol 的特殊处理后,就没有局限性。(推荐)
前两种方式网上都有文章,我们就单独说说最后一种
通过Reflect.construct()
来判断一个函数是否能够作为构造函数
function is_constructor(f){
// 特殊判断,Symbol 能通过检测
if(f === Symbol ) return false
try {
Reflect.construct(String,[],f)
}catch (e) {
return false
}
return true;
}
其实本质上也是用抛出异常方式来判断,但与直接 new A() 的抛异常方式不同的是,它不会触发构造函数的执行。这就得来看看Reflect.construct
Reflect.construct
方法等同于 new target(...args), 提供了一种不使用new来调用构造函数的方法
function A() {
this.a = 1
}
new A(); {a:1}
// 等于
Reflect.construct(A,[]); // {a:1}
Reflect.construct 可以接收一个可选的第三个参数
Reflect.construct(target, argumentsList[, newTarget])
- target: 被调用的构造函数
- argumentsList: 参数列表,类数组类型数据
- new Target: 可选,当传入时,使用newTarget.prototype 来作为实例对象的 prototype。否则使用 target.prototype
- 当 target 或者 newTarget 不能作为构造函数时,抛出 TypeError 异常
那么,我们可以怎么利用这些特性呢,先看使用原始 new 的方式
function A () {
console.log(1);
}
B = () =>{
console.log(2);
}
new A(); // 输出1
new B(); // TypeError, 抛出异常
// 使用抛异常方式来判断某个函数能否作为构造函数时,如果可以,那么构造函数就会先执行一遍
// 如果刚好在构造函数内处理一些业务代码,那么可能就会有副作用影响
function isConstructor(Fn){
try{
new A(); // 能够判断出 A 可以作为构造函数,但 A 会被先执行一次
// new B(); // 能够判断出 B 不能作为构造函数
} catch(e) {
return false
}
return true
}
那么,该如何来使用 Reflect.construct 呢
关键在于它的第三个参数,是用来指定构造函数生成的对象的 prototype,并不会去执行它,但却会跟第一个参数的构造函数一起经过能否作为构造函数的检查,来看看用法
function A () {
console.log(1)
}
A.prototype.a = 1;
function B () {
console.log(2)
}
B.prototype.a = 2
var a = Reflect.construct(A,[]); // 输出 1
a.a; // 1,继承自 A.prototype
var b = Rflect.construct(A,[], B); // 输出 1
b.a; // 2,继承自 B.prototype;
我们来大概写一下 Reflect.construct 传入三个参数时的伪代码
Reflect.construct = function (target,args, newTarget){
check target has [[Construct]]
check newTarget has [[Construct]]
var obj = Object.create(newTarget ? newTarget.prototype : target.prototype)
var result = target.call(obj, ...args);
return result instanceof Object ? result : obj
}
第一个参数 target 和第三个参数 newTarget 都会进行是否能作为构造函数使用的检查,虽然 target 会被作为构造函数调用,但我们可以把待检查的函数传给第三个参数,而第一个参数随便传入一个无关但可用来作为构造函数使用不久好了,所以代码如下
function is_contructor(f) {
// 特殊处理,因为 symbol 能通过 Reflect.construct 对参数的检测
if(f === Symbol ) return false
try {
// 第一个 target 参数传入一个无关的构造函数,第三个参数传入一个待检测的函数
Reflect.construct(String,[],f)
} catch(e) {
return false
}
return true
}
// 当 f 可作为构造函数使用,Reflect.construct 就会正常执行,那么此时:
// Reflect.construct(String, [], f) 其实相当于执行了
// a.__proto__ = f.prototype
// 既不会让被检测函数先执行一遍,又可以达到利用引擎层面检测函数是否能作为构造函数的目的
最终,模拟new的实现代码
function _new(Fn,...args) {
function is_constructor(f) {
if(f === Symbol ) return false
try {
Reflect.construct(String, [], f)
} catch(e) {
return false
}
return true
}
// 1. 参数判断检测
let isFunction = typeof Fn === 'function';
if(!isFunction || !is_constructor(Fn)) {
throw new TypeError(`${Fn.name || Fn} is not a constructor`)
}
// 2. 创建一个继承构造函数 .prototype 的空对象
var obj = Object.create(Fn.prototype);
// 3. 让空对象作为函数 A 的上下文,并调用 A 同时获取它的返回值
let result = Fn.call(obj,...args)
// 4. 如果构造函数返回一个对象,那么直接return 它,否则返回内部创建的新对象
return result instanceof Object ? result : obj;
}
总结
关键的一些点
- 如何判断某个函数能否作为构造函数
- 构造函数又返回值时的处理
- 构造函数生成的对象的原型处理
网友评论