必須讓你一看就能明白系列之———JavaScript 中 new 例項化物件的實現原理?

掘金範兒發表於2019-03-30
我們在使用new操作符建立新例項的時候,其內部也就那麼四個步驟,網上很多文章也把這四步都講清楚了,並且程式碼也貼出來了,但是還是有朋友們看不懂,我就想著怎麼讓朋友們能一看就能明白。

先貼出一個網上的例子:

function New(func) { 
    var res = {}; 
    if (func.prototype !== null) { 
        res.__proto__ = func.prototype; 
    } 
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); 
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { 
        return ret; 
    } 
    return res; 
} 
var obj = New(A, 1, 2); 
// equals to 
var obj = new A(1, 2);複製程式碼

是不是看的不是很明白呢??彆著急,我帶你一點點深入!!


這裡要看明白,首先需要認識apply,說到apply,還有一個call,他們就是同父異母的兄弟,還有人會說這個 Array.prototype.slice.call這個是什麼鬼?


那好吧,我們就先做點鋪墊工作:
apply()、call()都是函式物件的一個方法,他們的作用都是改變函式的呼叫物件,也可以說改變this指向,先來看一下apply、call的用法吧,其原理我也會在本篇文章後面實現。
  • apply方法:
  1. 語法:apply([thisObj[,argArray]])
  2. 定義:應用某一物件的一個方法,用另一個物件替換當前物件。 說明:apply的第一個引數thisObj和call方法的一樣,第二個引數argArray為一個傳引數組。thisObj如果未傳,那麼 Global 物件被用作 thisObj。
  • call方法:
  1. 語法:call([thisObj[,arg1[, arg2[, [,.argN]]]]])
  2. 定義:呼叫一個物件的一個方法,以另一個物件替換當前物件。 說明:call 方法可以用來代替另一個物件呼叫一個方法。call 方法可將一個函式的物件上下文從初始的上下文改變為由 thisObj 指定的新物件。如果沒有提供 thisObj 引數,那麼 Global 物件被用作 thisObj。 arg1 … argN為被呼叫方法的傳參。
文字有時候便於理解,但是有時候不如程式碼來的實在,其實最好的方式就是將文字和程式碼結合起來一點點理解消化,也就能深刻記住了。
來看程式碼吧:

var name = 'globalName'; //定義一個全域性name

var obj = {
	name: 'objName'
}

var foo = {
	name: 'fooName',
	getName: function() {
		return this.name;
	}
}

console.log(foo.getName())             //  fooName
console.log(foo.getName.apply(obj));   //  objName
console.log(foo.getName.apply())       //  globalName
console.log(foo.getName.apply(window)) //  globalName

console.log(foo.getName())             //  fooName
console.log(foo.getName.call(obj));    //  objName
console.log(foo.getName.call())        //  globalName
console.log(foo.getName.call(window))  //  globalName
複製程式碼

 foo.getName()  
 //這裡foo呼叫自己的方法,返回自身name屬性值fooName,如果大家對於this指向還不清楚,請自行補課複製程式碼

foo.getName.apply(obj) 
//這裡通過使用apply方法切換getName函式執行的上下文環境,將this指向了obj,所以輸出了objName,有一種借殼生蛋的作用複製程式碼

foo.getName.apply()
//這裡在呼叫apply並沒有傳入需要指向的引數,預設全域性window物件複製程式碼

foo.getName.apply(window)
//這裡顯示的傳入window物件,將this指向了window,輸出了globalName複製程式碼

相信大家已經明白了apply的用法,call也是同樣的道理,這裡我們只用到了apply和call方法的第一個引數,我們再看看他們第一個引數後面的引數怎麼回事?

通過apply和call實現陣列追加:

var arr1 = [1,2,3,4];
Array.prototype.push.apply(arr1, [5,6,7]);  //呼叫陣列原型上的push方法,相當於是arr1借用了push方法實現尾部追加元素,第二個元素是以陣列形式
console.log(arr1)  //[1, 2, 3, 4, 5, 6, 7]複製程式碼

var arr1 = [1,2,3,4];
Array.prototype.push.call(arr1,5,6,7); //而call方法是已引數列表形式傳入追加的元素
console.log(arr1)  //[1, 2, 3, 4, 5, 6, 7]複製程式碼

以上就是apply和call的使用方法和區別了,要想更好的理解呼叫new例項化物件具體做了什麼,我們還需要了解一下JavaScript建立物件的幾種方式,通過比對我們來發現其中的原理。
  • 工廠模式

function createPerson(name,age,job) {    
    var obj = new Object(); //建立一個物件    
    obj.name = name; //給物件新增屬性 和方法    
    obj.age = age;    
    obj.job = job;    
    obj.sayName = function() {        
        console.log(this.name)    
    }    
    return obj; //返回這個物件
}

var person1 = createPerson("lili", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

console.log(person1 instanceof createPerson)  //false
console.log(person2 instanceof createPerson)  //false複製程式碼

優點:解決了建立多個物件的時候的重複程式碼問題。

缺點:不能解決物件識別問題,也就是不知道一個物件的型別,上面的instanceof 說明問題。


ECMAScript中的建構函式可用來建立特定型別的物件。像Object和Array這樣的原生建構函式,在執行時會自動出現在執行環境中,我們可以建立自定義的建構函式,從而定義自定義物件型別的屬性和方法。下面就是使用建構函式模式將前面的例子重寫如下:

  • 建構函式模式

function Person(name,age,job) {    
    this.name = name;    
    this.age = age;    
    this.job = job;    
    this.sayName = function () {        
        console.log(this.name);    
    }
}
var person1 = new Person("lili", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1 instanceof Person); // true 這裡可以判斷其屬於Person型別的例項物件了
console.log(person2 instanceof Person); // true複製程式碼

我們可以注意到,Person()中的程式碼除了與createPerson()中相同的部分外,還存在以下不同之處:

  1. 沒有顯示的建立物件;
  2. 直接將屬性和方法賦給了this物件;
  3. 沒有return語句。

注意: 通過new例項化的物件,我們就可以明確知道了其型別,

要建立Person的新例項,必須使用new操作符,那麼new的過程中都經歷了那幾個步驟呢:

  1. 建立一個新物件;
  2. 將建構函式的作用域賦值給新物件(因此this就指向了這個新物件);
  3. 執行建構函式中的程式碼(為這個新物件新增屬性);
  4. 返回新物件;


我們嘗試自己用程式碼來實現一下new過程吧!!!

function New(Person,name,age,job) { //Person是上面那個建構函式   
    //1.建立一個物件,    
    var obj = {};    
    //2.將建構函式的作用域賦給新物件,因此this就指向了這個新物件,這裡我們將obj的__proto__指向了Person的prototype,因為通用new出來的例項的__proto__屬性都指向建構函式的原型(prototype) 
    obj.__proto__ = Person.prototype;
    //執行建構函式Person中的程式碼,這裡通過apply將作用域切換為當前obj,這裡的arguments是New方法傳入的引數,通過slice去掉第一個引數,傳入剩下的引數,    
    var ret = Person.apply(obj,Array.prototype.slice.call(arguments,1));
    // 如果ret是物件或者是函式,就返回,如果不是就返回obj;
    if((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {        
        return ret;    
    }
    return obj;
}
var o = New(Person,'jiji',1,'mother');
console.log(o)複製程式碼

不知道大家看明白了沒有,歡迎留言交流~~~~

碼字不易,如果對你有幫助的話,不忙點個贊再走~~~~~




相關文章