在學習的過程中,不可避免的會遇到new
操作符,這次就來好好刨根問底一下,也算是加深理解和記憶了。
mdn中是這麼定義new
操作符的:
new 運運算元建立一個使用者定義的物件型別的範例或具有建構函式的內建物件的範例。
在這句話裡我們來看一個關鍵詞:具有建構函式
。這是個什麼意思呢?我們先通過幾個例子來看一下:
//例1let Animal1=function(){this.name=1};let animal=new Animal1; //這裡不帶()相當於不傳引數//=>Animal1 {name: 1}//例2let 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!我們再來看一個例子:
//例3let 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.
意思就是,方法不能是建構函式,如果嘗試建立一個方法的範例,就會丟擲型別錯誤。這樣說就懂了,但是還沒完,這個說法沒有完全解釋清楚原理,我們再看個例子:
//例4const 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 constructornew example.Shorthand(); // Uncaught TypeError: example.Shorthand is not a constructor複製程式碼
對照這個例子,我們在ECMA規範查閱,發現所有的函數在建立時都取決於FunctionCreate
函數:
FunctionCreate (kind, ParameterList, Body, Scope, Strict, prototype)
- If the prototype argument was not passed, then let prototype be the intrinsic object %FunctionPrototype%.
- If "kind" is not Normal, let allocKind be "non-constructor".
這個函數的定義可以看出
Normal
的函數被建立時,它才是可構造的函數,否則他就是不可構造的。在我們這個例子中,Arrow
的型別為Arrow
,而ShortHand
的型別是Method
,因此都不屬於可構造的函數,這也解釋了例3所說的"方法不能作為建構函式"。
搞清楚了new
操作符可以操作的目標,終於可以神清氣爽的來看看它的作用了(不容易呀TAT)。
我們舉一個簡單的例子來具體看看它的作用:
function Animal(name){ this.name=name; console.log("create animal"); }let animal=new Animal("大黃"); //create animalconsole.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
函數原型上的方法,說明Animal
在animal
物件的原型鏈上,那麼在哪一層呢?我們驗證一下:
animal.__proto__===Animal.prototype; //true複製程式碼
那我們就知道了,animal
的__proto__
直接指向了Animal
的prototype
。
除此之外,如果我們在建構函式的函數體裡返回一個值,看看會怎麼樣:
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
操作符吧~
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
操作符不難,更重要的還是去理解這個過程,明白其中的原理。
更多相關免費學習推薦:(視訊)
以上就是JavaScript之 這次徹底搞懂new操作符!的詳細內容,更多請關注TW511.COM其它相關文章!