模擬實現apply/call/bind

Jarva發表於2019-02-22

call()apply()的區別在於,call()方法接受的是若干個引數的列表,而apply()方法接受的是一個包含多個引數的陣列

call

需滿足:

1.改變this的指向。

2.新物件可以執行該函式。

3.考慮this為null和undefined時,this指向window。 this為基本型別時,原生的call會用Object自動轉換。

4.能傳入引數。

Function.prototype.call1 = function (context) {
	context = context ? Object(context) : window // 實現3
	// 模擬傳入的物件中有一個呼叫該物件的函式
	// 作用是為了改變函式的作用域指向該物件
	context.fn = this
	
	//接收引數,若有。
	let args = [...arguments].slice(1) // 第0個為this
	let result = context.fn(...args) // 執行fn
	delete context.fn //刪除fn
	
	return result
}
複製程式碼

測試一下:

var value = 111;

var obj = {
    value: 999
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

function foo() {
    console.log(this);
}

bar.call1(null); // 111
foo.call1(520); // Number {520, fn: ƒ}

bar.call1(obj, 'jarva', 3);
// 999
// {
//   age: 3
//	 name: "jarva"
//	 value: 999
//  }
複製程式碼

apply

與call實現基本一致

Function.prototype.apply1 = function (context) {
    context = context ? Object(context) : window
    context.fn = this
  
    let result;
    if (arguments[1]) {
		result = context.fn(...arguments[1])  
    } else {
        result = context.fn()
    }
      
    delete context.fn
    return result;
}
複製程式碼

bind

bind() 方法會建立一個新函式,當這個新函式被呼叫時,它的 this 值是傳遞給 bind() 的第一個引數,傳入bind方法的第二個以及以後的引數加上繫結函式執行時本身的引數按照順序作為原函式的引數來呼叫原函式。bind返回的繫結函式也能使用 new 操作符建立物件:這種行為就像把原函式當成構造器,提供的 this 值被忽略,同時呼叫時的引數被提供給模擬函式。

bind 方法與 call / apply 最大的不同就是前者返回一個繫結上下文的函式,而後兩者是直接執行了函式。

需滿足:

1.指定this。

2.返回函式。

3.傳入引數。

4.對new型別時,需要忽略this。

現在先考慮前三個條件,實現如下:

Function.prototype.bind1 = function(context) {
	let _this = this // 記住當前作用域,指向呼叫者。

	let args = Array.prototype.slice.call(arguments, 1) // 去掉第一個this引數
	// let args = [].slice.call(arguments, 1)
	// let args = [...arguments].slice(1)
	
	return function () {
        // 因為 bind 可以實現類似這樣的程式碼 fn.bind(obj, 1)(2) 
	    // 所以要合併返回引數
		// let bindArgs = Array.prototype.slice.call(arguments);
		let bindArgs = [...arguments]
		return _this.apply(context, args.concat(bindArgs)) // 指定this。
	}
}
複製程式碼

測試用例

var value = 111

var foo = {
    value: 999
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
}

var bindFoo = bar.bind1(foo, "Jack")
bindFoo(20);
//{ age: 20, name: "Jack", value: 999 }
複製程式碼

一個繫結函式也能使用new操作符建立物件:這種行為就像把原函式當成構造器,提供的 this 值被忽略,同時呼叫時的引數被提供給模擬函式。

現在來實現完整的bind模擬。

Function.prototype.bind2 = function(context) {

	// 如果呼叫的不是函式,要丟擲異常
	if (typeof this !== 'function') {
	    throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
	}
	let _this = this // 記住當前作用域,指向呼叫者。

	let args = Array.prototype.slice.call(arguments, 1) // 去掉第一個this引數
	// let args = [].slice.call(arguments, 1)
	// let args = [...arguments].slice(1)
	var fn = function () {
        // 因為 bind 可以實現類似這樣的程式碼 fn.bind(obj, 1)(2) 
	    // 所以要合併返回引數
		// let bindArgs = Array.prototype.slice.call(arguments)
		let bindArgs = [...arguments]
		// 當作為建構函式時,this 指向例項,此時 this instanceof fBound 結果為 true,可以讓例項獲得來自繫結函式的值
		return _this.apply(this instanceof fn ? this : context, args.concat(bindArgs)) // 指定this。
	}
	// 還要考慮修改返回函式的prototype為繫結函式的prototype,
	// 使得例項可以繼承原型的值。 
	// 為了修改fn.prototype時不影響原型的值,使用ES5的 Object.create()方法生成一個新物件
	fn.prototype = Object.create(this.prototype)
	return fn
}	

複製程式碼

測試一下

// 測試用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正確
// undefined
// Jack
// 20

obj.habit; // 返回正確
// shopping

obj.friend; // 返回正確
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // kevin

複製程式碼

收工~

相關文章