javascript模擬new的實現

剪雲者發表於2019-02-26

在日常使用new時,我們很清楚它的作用。

準備工作

我們先建立一個Person類,他接受兩個引數name姓名和age年齡:

function Person(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayHello = function(){
    console.log('我叫' + this.name + ', 今年' + this.age + '歲了');
}
複製程式碼

new 的使用

我們先用new例項化一個person,並列印出來,看看結構。

var person = new Person('小明', 20);
console.log(person); // Person {name: "小明", age: 20}
person.sayHello(); // 我叫小明, 今年20歲了
複製程式碼

最終person的結果是一個object

javascript模擬new的實現

模擬過程

結合Person方法,我們容易發現,彷彿有一個object替代了this的位置,執行了賦值操作,輸出了最後的結果。

1、替代this賦值

思考替代過程:

    // 1、建立了一個物件
    var result = {}; 
    
    // 2、物件代替了this的位置,執行了賦值
    {
        result.name = "小明";
        result.age = 20;   
    } 
    
    // 3、輸出 {name: "小明", age: 20}
    return result; 
複製程式碼

那麼問題來了,我該如何將這個result替代this的位置呢?

這就用到了call或者apply:

    var result = {};
    Person.call(result, '小明', 20);
    result; // {name: "小明", age: 20}
複製程式碼

這樣我們就完成了第一步,你可以在console控制檯中嘗試一下!

接下來就是處理原型部分了。

2、原型移植

這就很簡單了,我們有很多辦法:

//方案1
result.__proto__ = Person.prototype; //有一定副作用(可列舉)
//方案2
Object.setPrototypeOf(result, Person.prototype);
//方案3
result = Object.create(Person.prototype);
複製程式碼

3、初步結果

綜上我們容易整理出這樣的結果:

    function likeNew(fn){
        //我們先完成原型移植,以免建構函式中呼叫了原型方法。
        var result = Object.create(fn.prototype);
        var args = [].slice.call(arguments,1);
        fn.apply(result, args);
        return result
    }
    var person = likeNew(Person, '小明', 20); 
    console.log(person); // Person {name: "小明", age: 20}
    person.sayHello(); // 我叫小明, 今年20歲了
複製程式碼

雖然我們按照思路是先建立物件->執行->處理原型。
但是實際上正確的順序是 建立包含對應原型的物件->執行。

問題

如果被例項化的方法如果本身包含返回值,new的結果會是什麼呢?

    function Person(name, age){
        this.name = name;
        this.age = age;
        return name
    }
    var person = new Person('張三', 20);
    console.log(person); // ?
    var person1 = new Person(['張三'], 20);
    console.log(person); // ?
複製程式碼

通過嘗試,輸出結果分別為Person {name: '張三', age: 20}['張三']
為什麼會產生完全不同的結果呢?
猜想:方法返回值的型別決定例項化後的結果

1、基本型別

js中的基本型別有number、string、boolean、undefined、null、symbol(es6)共6種。

    function Test(value){
        this.value = value;
        return value;
    }
    //number
    var number = new Test(123);
    console.log(number); // Test {value: 123}
    
    //string
    var string = new Test('abc');
    console.log(string); // Test {value: 'abc'}
    
    //boolean
    var boolean = new Test(true);
    console.log(boolean); // Test {value: true}
    
    //undefined
    var Undefined = new Test();
    console.log(Undefined); // Test {value: undefined}
    
    //null
    var Null = new Test(null);
    console.log(Null); // Test {value: null}
    
    //symbol
    var symbol = new Test(Symbol('key'));
    console.log(symbol); // Test {value: Symbol(key)}
複製程式碼

上述例子所有返回值均為例項化後的物件,由此可見,所有基本型別返回的都是正常的。

2、引用型別

js中的引用型別有 object、array、function。我們接著上面的Test類繼續建立物件:

    //object
    var object = new Test({}});
    console.log(object); // {}
    
    //array
    var array = new Test([]);
    console.log(array); // []
    
    //function
    var functions = new Test(function(){});
    console.log(functions); // function(){}
    
    //特殊的number
    var number = new Test(new Number(1));
    console.log(number); // Number {1}
    console.log(typeof number); // object。
複製程式碼

可見,方法的返回值若為引用型別,new操作符就“失效”了。
那他真的失效了嗎?讓我們看看方法內部執行的過程:

    function Test(value){
        this.value = value;
        console.log(this);
        return [1,2,3];
    }
    var result = new Test(1); // Test {value: 1}
    console.log(result);// [1, 2, 3]
複製程式碼

由此可見,this在Test的例項化過程中,確實被建立了,只不過由於Test本身的返回值為引用型別,所以例項化後的結果被其替換了。

最後的整理

根據以上的推論,再次完善了likeNew:

    function likeNew(fn){
        var result = Object.create(fn.prototype);
        var args = [].slice.call(arguments,1);
        var fnResult = fn.apply(result, args);
        if(typeof fnResult === 'object' || typeof fnResult === 'function' && fnResult !== null){
            return fnResult
        }
        return result
    }
    
    var person = likeNew(Person, '小明', 20); 
    console.log(person); // Person {name: "小明", age: 20}
    person.sayHello(); // 我叫小明, 今年20歲了
    
    //原始型別 number
    var number = likeNew(Test, 1);
    console.log(number); // Test {value: 1}
    
    var Null = likeNew(Test, null);
    console.log(Null); // Test {value: null}
    
    //引用型別
    var object = likeNew(Test, {});
    console.log(object); // {}
    
    var numberObject = likeNew(Test, new Number(1));
    console.log(numberObject); // Number{1}
複製程式碼

總結:
1、new操作符在進行例項化時,首先會建立一個包含指定__proto__的物件,再帶入方法中執行,並選擇性輸出此物件。
2、被操作的方法的返回值若為引用型別,則會替換原本例項化的結果。

以上如有不當請指出。

相關文章