学习JS底层---手写new初体验

76 阅读6分钟

前言

最近,作者在深度学习JavaScript的知识,其中,作者学习了手写new的方法后,非常感兴趣,所以准备写一篇文章来分享一下手写new的知识!


传统new需要做什么?

让我们先想想,我们使用js的new方法实际上做了哪些事情?

function Person(name,age){
    this.name = name
    this.age = age
}

var person = new Person('张三',18)
  1. 咱们首先创建了一个空对象{}

  2. 将该Person(){...}里面的this指向该对象{},所以对象{}的this.name = 传入的张三,对象{}的this.age = 传入的18

  3. 构造了该对象的原型链,让对象{}的__proto__为构造函数Person的prototype

  4. 返回该对象

我们输出new方法得到的结果如下

image.png

这是在Person构造函数我们正常写没有返回值时做的事,但如果我们在Person中写一个return 会发生什么事呢?

  • 当return一个对象
function Person(name,age){
    this.name = name
    this.age = age
    return {
        name:name,
        label:'hah',
        emotion: 'happy'
    }
}

此时我们输出person的结果如下

image.png

你会发现返回的结果是我们return 里面的内容,而不是正常应该返回的空对象

  • 当return一个简单数据类型
function Person(name,age){
    this.name = name
    this.age = age
    return 1
}

此时我们再查看返回的内容

image.png

你会发现,它会无视这个返回的简单数据类型,正常返回我想要的内容。

上面提到的这两种添加了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 返回 nullobjectFactory 仍然会返回新创建的 obj,而不是 null

image.png

下面给出作者对这个情况的处理:

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,在面试官前秀一波!