前言
最近,作者在深度学习JavaScript的知识,其中,作者学习了手写new的方法后,非常感兴趣,所以准备写一篇文章来分享一下手写new的知识!
传统new需要做什么?
让我们先想想,我们使用js的new方法实际上做了哪些事情?
function Person(name,age){
this.name = name
this.age = age
}
var person = new Person('张三',18)
-
咱们首先创建了一个空对象{}
-
将该Person(){...}里面的this指向该对象{},所以对象{}的this.name = 传入的张三,对象{}的this.age = 传入的18
-
构造了该对象的原型链,让对象{}的__proto__为构造函数Person的prototype
-
返回该对象
我们输出new方法得到的结果如下
这是在Person构造函数我们正常写没有返回值时做的事,但如果我们在Person中写一个return 会发生什么事呢?
- 当return一个对象
function Person(name,age){
this.name = name
this.age = age
return {
name:name,
label:'hah',
emotion: 'happy'
}
}
此时我们输出person的结果如下
你会发现返回的结果是我们return 里面的内容,而不是正常应该返回的空对象
- 当return一个简单数据类型
function Person(name,age){
this.name = name
this.age = age
return 1
}
此时我们再查看返回的内容
你会发现,它会无视这个返回的简单数据类型,正常返回我想要的内容。
上面提到的这两种添加了return 后的方式,我们都需要注意,等下在实现时需要注意这两点
手写new的方式
好了,接下来就让我们开始手写new吧,温馨提示,手写New涉及到很多js的知识点,有点复杂哦,请慢慢分析!
1.构造一个手写new的函数
我们定义函数objectFactory来替代传统的new,实现手写new。
请你思考,该函数的参数该如何定义?
想想我们传统的new Person这种方式,传递的参数往往是不确定的,所以我们在objectFactory中对参数的处理,采用argments的方式处理:
定义objectFactory时不需要指定参数,在内部用argments来处理,这样子无论有多少个参数,我们都能处理了。
function objectFactory() {
console.log(arguments) // [Function: Person]、'张三'、18
}
function Person(name,age){
this.name = name
this.age = age
}
let person = objectFactory(Person,'张三',18)
2.创建一个新对象
对应着上面的步骤,我们先创建一个新对象var obj = {}待会对这个对象进行处理,这个obj是我们最终要返回的
⭐目前的objectFactory函数:
function objectFactory() {
var obj = {}
}
3. 返回构造函数
这一步我们需要把参数中的第一项:构造函数给拿出来,因为等下我们针对构造函数可能要做一些操作,比如指定原型链。
我们使用以下代码:
var Constructor = [].shift.call(arguments) // 返回构造函数
这行代码有点复杂,让我们详细分析:
先看右边,由于arguments不支持直接使用shift方法,所以我们需要借助别的方式来让arguments来使用shift方法:在arguments中删除第一项构造函数(比如传入的Person),并且将删除的构造函数返回给Constructor,我们需要借助call来实现
[]在使用shift时,this的指向是[]这个对象的本身,举个例子,比如[1,2,3,4].shift(),此时this是[1,2,3,4],shift所以会删除1并且返回1,但是我们要使用call,让this不指向[],而是指向arguments。这样子,shift操作实际上的对arguments进行的。
所以此时就可以达到我们想要的效果:删除arguments中的第一项,并且将其返回,此时Constructor就是Person构造函数,而arguments现在只剩下除了Person以外的参数(比如'name'、'age')>
⭐目前的objectFactory函数:
function objectFactory() {
var obj = {}
var Constructor = [].shift.call(arguments)
}
4.原型链操作
接下来,我们来实现将obj的__proto__设置为构造函数的prototype。
因为上面第三步我们已经拿到了构造函数objectFactory,所以进行下面的操作即可
obj.__proto__ = Constructor.prototype
⭐目前的objectFactory函数:
function objectFactory() {
var obj = {}
var Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
}
5.更改this指向
现在我们要做的就是将我们声明构造函数里面的this指向变成我们的最终返回的对象obj。
这里我们既可以使用apply,也可以使用call,我就使用apply了
Constructor.apply(obj,arguments);
可以看到,我们执行了Constructor,并且将arguments里面的this指向都变成了obj
⭐目前的objectFactory函数:
function objectFactory() {
var obj = {}
var Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
Constructor.apply(obj,arguments);
}
6.返回对象
最后一步就是要将经过上面一系列操作过后的obj对象给返回。
正常情况下我们直接return obj即可。
⭐最终的objectFactory函数以及测试结果:
function objectFactory() {
var obj = {}
var Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
Constructor.apply(obj,arguments);
return obj
}
function Person(name,age){
this.name = name
this.age = age
}
let person = objectFactory(Person,"张三",18)
console.log(person) // Person { name: '张三', age: 18 }
处理返回对象
上面的过程是我们正常书写构造函数师的过程,如果我们此时在Person中return了值,那么根据之前提到的规则,此时输出的结果也会不同,所以我们就需要优化objectFactory,让它能cover这些情况。
return 对象
如果返回的是一个对象,比如{name:'Im a obj'},那么objectFactory函数就要返回该对象
return 简单数据类型
如果返回的是简单数据类型,我们的处理逻辑是,不影响正常返回obj对象
故我们更改后的函数如下:
function objectFactory() {
var obj = {}
var Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
var res = Constructor.apply(obj,arguments); //执行构造函数后 接受return的结果
return typeof res === 'object'? res:obj; //用三目运算符 执行判断逻辑
}
retun Null
如果我们这样写的话,return null的话想想会怎么样,因为null在js中也是对象,所以按理来讲,会返回null对象? 还是有别的坑?让我们运行看看
运行后我们发现,确实有坑,即使 Person 返回 null,objectFactory 仍然会返回新创建的 obj,而不是 null。
下面给出作者对这个情况的处理:
return typeof res === 'object'? res || obj:obj; //用||来实现null的特判
这是一个很巧妙的写法,你可以看到在这样书写过后,使用||的写法,当return null时,也能很好地处理了!
⭐所以最终的函数代码objectFactory如下:
function objectFactory() {
var obj = {};
var Constructor = [].shift.call(arguments)
obj.__proto__ = Constructor.prototype
var res = Constructor.apply(obj,arguments);
return typeof res === 'object'? res || obj : obj;
}
ES6写法
上面是对objectFactory函数的es5的写法,实际上我们可以用ES6的新特性展开表达式来更方便地书写,下面是ES6的具体写法:
function objectFactory(Constructor, ...args) {
const obj = {};
obj.__proto__ = Constructor.prototype;
const res = Constructor.apply(obj, args); // Execute constructor and get return result
return typeof res === 'object' ? res || obj : obj;
}
使用ES6的...展开运算符后,我们不再需要 [].shift.call(arguments) 来提取构造函数,直接使用参数解构 (Constructor, ...args)。
而且我们用const替代 var,这是更符合现代 JS 的变量声明方式。
总结
以上就是手写new的全部过程以及讲解,相信看完这篇文章之后,你也能够去手写js中的new,在面试官前秀一波!