【JavaScript】这次彻底搞懂new操作符!

前言

在学习JavaScript的过程中,不可避免的会遇到new操作符,这次就来好好刨根问底一下,也算是加深理解和记忆了。

什么是new操作符?

mdn中是这么定义new操作符的:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

在这句话里我们来看一个关键词:具有构造函数。这是个什么意思呢?我们先通过几个例子来看一下:

//例1
let Animal1=function(){this.name=1};
let animal=new Animal1; //这里不带()相当于不传参数
//=>Animal1 {name: 1}

//例2
let TestObj={}
let t1=new TestObj;
//=>Uncaught TypeError: TestObj is not a constructor

我们可以看到,例1成功的执行了new语句,创建出了实例。例2在new一个{}对象时报错TypeError: TestObj is not a constructor,指出目标不是一个constructor。为什么普通的对象就不能执行new操作符呢?在ECMA规范里有相关的介绍:

If Type(argument) is not Object, return false.
If argument has a [[Construct]] internal method, return true.
Return false.

意思就是:

  • 构造函数首先得是一个对象,否则不满足条件
  • 其次,对象必须拥有[[Construct]]内部方法,才可以作为构造函数

我们这里的{}就是一个对象,满足第一个条件,那么显然,肯定是因为{}没有[[Construct]]这个内部方法,所以无法使用new操作符进行构造了。

那么我们已经搞定了new操作符的可操作对象,是不是可以去看看它的作用了呢?答案是:NO!我们再来看一个例子:

//例3
let testObj={
    Fn(){
        console.log("构造成功!")
    }
}
let t3=new testObj.Fn;
//=>Uncaught TypeError: testObj.Fn is not a constructor

what?为什么刚刚还能成功构造的函数,作为方法就不行了呢?其实在MDN中也有直接介绍:

Methods cannot be constructors! They will throw a TypeError if you try to instantiate them.

意思就是,方法不能是构造函数,如果尝试创建一个方法的实例,就会抛出类型错误。这样说就懂了,但是还没完,这个说法没有完全解释清楚原理,我们再看个例子:

//例4
const example = {
  Fn: function() { console.log(this); },
  Arrow: () => { console.log(this); },
  Shorthand() { console.log(this); }
};
new example.Fn();        // Fn {}
new example.Arrow();     // Uncaught TypeError: example.Arrow is not a constructor
new example.Shorthand(); // Uncaught TypeError: example.Shorthand is not a constructor

对照这个例子,我们在ECMA规范查阅,发现所有的函数在创建时都取决于FunctionCreate函数:

FunctionCreate (kind, ParameterList, Body, Scope, Strict, prototype)

  1. If the prototype argument was not passed, then let prototype be the intrinsic object %FunctionPrototype%.
  2. If "kind" is not Normal, let allocKind be "non-constructor".

这个函数的定义可以看出

  • 只有当类型为Normal的函数被创建时,它才是可构造的函数,否则他就是不可构造的。

在我们这个例子中,Arrow的类型为Arrow,而ShortHand的类型是Method,因此都不属于可构造的函数,这也解释了例3所说的"方法不能作为构造函数"。
搞清楚了new操作符可以操作的目标,终于可以神清气爽的来看看它的作用了(不容易呀TAT)。

new操作符实现了什么?

我们举一个简单的例子来具体看看它的作用:

function Animal(name){
    this.name=name;
    console.log("create animal");
}

let animal=new Animal("大黄");  //create animal

console.log(animal.name);       //大黄

Animal.prototype.say=function(){
    console.log("myName is:"+this.name);
}
animal.say();                   //myName is:大黄

我们从这个例子来分析一下,首先我们看这一句:

let animal=new Animal("大黄");

可以看到,执行new操作符后,我们得到了一个animal对象,那么我们就知道,new操作符肯定要创建一个对象,并将这个对象返回。再看这段代码:

function Animal(name){
    this.name=name;
    console.log("create animal");
}

同时我们看到结果,确实输出了create animal,我们就知道,Animal函数体在这个过程中被执行了,同时传入了参数,所以才执行了我们的输出语句。但我们的函数体里还有一句this.name=name体现在哪里呢?就是这一句:

console.log(animal.name);       //大黄

执行完函数体后,我们发现返回对象的name值就是我们赋值给this的值,那么不难判断,在这个过程中,this的值指向了新创建的对象。最后还有一段:

Animal.prototype.say=function(){
    console.log("myName is:"+this.name);
}
animal.say();                   //myName is:大黄

animal对象调用的是Animal函数原型上的方法,说明Animalanimal对象的原型链上,那么在哪一层呢?我们验证一下:

animal.__proto__===Animal.prototype; //true

那我们就知道了,animal__proto__直接指向了Animalprototype
除此之外,如果我们在构造函数的函数体里返回一个值,看看会怎么样:

function Animal(name){
    this.name=name;
    return 1;
}
new Animal("test"); //Animal {name: "test"}

可以看到,直接无视了返回值,那我们返回一个对象试试:

function Animal(name){
    this.name=name;
    return {};
}
new Animal("test"); //{}

我们发现返回的实例对象被我们的返回值覆盖了,到这里大致了解了new操作符的核心功能,我们做一个小结。

小结

new操作符的作用:

  • 创建一个新对象,将this绑定到新创建的对象
  • 使用传入的参数调用构造函数
  • 将创建的对象的_proto__指向构造函数的prototype
  • 如果构造函数没有显式返回一个对象,则返回创建的新对象,否则返回显式返回的对象(如上文的{}

模拟实现一个new操作符

说了这么多理论的,最后我们亲自动手来实现一个new操作符吧~

var _myNew = function (constructor, ...args) {
    // 1. 创建一个新对象obj
    const obj = {};

    //2. 将this绑定到新对象上,并使用传入的参数调用函数

    //这里是为了拿到第一个参数,就是传入的构造函数
    // let constructor = Array.prototype.shift.call(arguments);
    //绑定this的同时调用函数,...将参数展开传入
    let res = constructor.call(obj, ...args)

    //3. 将创建的对象的_proto__指向构造函数的prototype
    obj.__proto__ = constructor.prototype

    //4. 根据显示返回的值判断最终返回结果
    return res instanceof Object ? res : obj;
}

上面是比较好理解的版本,我们可以简化一下得到下面这个版本:

function _new(fn, ...arg) {
    const obj = Object.create(fn.prototype);
    const res = fn.apply(obj, arg);
    return res instanceof Object ? res : obj;

大功告成!

总结

本文从定义出发,探索了new操作符的作用目标和原理,并模拟实现了核心功能。其实模拟实现一个new操作符不难,更重要的还是去理解这个过程,明白其中的原理。

写在最后

本人小白一个,可能有些资料查的不是很全或是有错误,欢迎指出,一定会及时改正。如果觉得这篇文章对你有帮助的话,不妨点个赞支持一下,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352