面試的重點難點的坑來啦!~/(ㄒoㄒ)/~~不出意外,this在ES5中是比較頭疼和讓初學者恐懼的一塊,儘管在 ES6 中可能會極大避免 this 產生的錯誤,可是爲了前端初學者可以在使用上可以將call,apply,bind等容易混淆的this指向問題,最好仍是瞭解一下call、apply、bind 三者的區別,以及它們在底層中是如何來實現的~前端
全部函數能調call、apply.bind的方法前提是function是Function的實例,而Function.prototype上面有這三個方法es6
Function.prototype = {
call
apply
bind
}
複製代碼
用法:
第一個參數就是改變的this指向,寫誰就是誰(特殊:非嚴格模式下,傳遞null/undefined指向的也是window)
區別:
執行函數,傳遞的參數方式有區別,call是一個個傳遞,apply是把須要傳遞的參數放到數組中總體傳遞
面試
func.call([context],10,20);
func.apply([context],[10,20])
複製代碼
用法:
bind不是當即執行函數,屬於預先改變this和傳遞一些內容 => "柯里化思想"
區別:
call/apply都是改變this的同時直接將函數執行,而bind須要手動執行
數組
let obj = {
fn(x, y) {
console.log(this, x, y);
}
}
obj.fn.call(); // window 嚴格模式下: undefined
obj.fn.call(null); // ...
obj.fn.call(undefined); // ...
obj.fn.call(window, 10, 20); // window
obj.fn.apply(window, [10, 20]); // window
複製代碼
錯誤寫法:
setTimeout(obj.fn.call(window, 10, 20));
複製代碼
緣由:
fn.call()自動執行,執行以後將結果(window)賦值給setTimeout再讓瀏覽器執行,顯然是錯誤的,因setTimeout第一個參數應爲要執行的函數
,而非window等表達式瀏覽器
正確寫法:
setTimeout(obj.fn.bind(window, 10, 20));
複製代碼
注:
重寫bind須要在Function.prototype
定義,由於是Function原型上的方法
柯里化思想:一個大函數裏面返回一個小函數,返回的小函數供外面調取使用,在執行大函數執行時造成的執行上下文不能銷燬,造成閉包,保護大函數裏面的變量,等到anonymous(下文提到)執行時,再調取大函數裏面的變量
基礎版
bash
~ function anonymous(proto) {
// context: bind更改以後的this指向
function bind(context) {
// context may be null or undefined
if (context == undefined) {
context = window;
}
<!--arguments { 0:context, 1:10, 2:20, length:3}-->
<!--獲取傳遞的實參集合-->
var args = [].slice.call(arguments, 1);
須要最終執行的函數(例: obj.fn)
var _this = this;
<!--bind()執行會返回一個新函數-->
return function anonymous() {
_this.apply(context, args);
}
proto.bind = bind;
}
}(Function.prototype);
let obj = {
fn(x, y) {
console.log(this, x, y);
}
}
複製代碼
如今bind原理懂了以後,咱們來回顧一下這個題
回顧:在1秒鐘以後,執行fn函數,讓其函數裏的this變爲window
bind結合setTimeout實現
原理:
一、1s以後先執行bind的返回結果anonymous
二、在anonymous中再把須要執行的obj.fn執行,把以前存儲的context/args傳遞給函數閉包
setTimeout(obj.fn.bind(window, 10, 20));
setTimeout(anonymous, 1000);
複製代碼
完整版
app
// document.body.onclick = obj.fn.bind(window, 10, 20);
document.body.onclick = anonymous;
複製代碼
例:給當前元素的某個事件行爲綁定方法,當事件觸發執行完這個方法以後,方法中有一個默認事件對象ev(MouseEvent),ev做爲anonymous的形參對象anonymous(ev),由於最終執行的是obj.fn,因此爲了方便拿到ev
函數
~ function anonymous(proto) {
// context: bind更改以後的this指向
function bind(context) {
// context may be null or undefined
if (context == undefined) {
context = window;
}
<!--arguments { 0:context, 1:10, 2:20, length:3}-->
<!--獲取傳遞的實參集合-->
var args = [].slice.call(arguments, 1);
須要最終執行的函數(例: obj.fn)
var _this = this;
<!--bind()執行會返回一個新函數-->
return function anonymous(ev) {
args.push(ev);
_this.apply(context, args);
}
proto.bind = bind;
}
}(Function.prototype);
let obj = {
fn(x, y,ev) {
console.log(this, x, y,ev);
}
};
複製代碼
因爲anonymous不必定綁給誰,因此不必定有ev,但也還有多是其餘東西,因此...性能
...
...
return function anonymous() {
var amArg = [].slice.call(arguments, 0);
args = args.concat(amArg);
_this.apply(context, args);
}
proto.bind = bind;
複製代碼
function bind (context = window, ...args) {
return (...amArg) => {
args = args.concat(amArg);
_this.apply(context, args);
}
}
複製代碼
經測試:apply在傳遞多個參數的狀況下,性能不如call,故改寫call
function bind (context = window, ...args) {
return (...amArg) => {
args = args.concat(amArg);
_this.call(context, ...args);
}
}
複製代碼
以obj.fn
.call(window, 10, 20)爲例
context.$fn = this
一、把當前函數(要更改的函數obj.fn),做爲context一個屬性,賦給this
二、context.&fn(),this天然指向context
三、防止對象屬性被竄改,及時delete context.$fn
四、call()執行以後應返回一個function,賦值給result
PS:(若是在面試的時候想寫詳細點能夠限定context數據類型爲引用類型,排除掉基本類型的可能)
~ function anonymous(proto) {
// 只有當context不傳,或傳undefined時,纔是window
function call(context = window, ...args) {
// 因此應該null狀況考慮進去
context === null ? context = window : null;
let type = typeof context;
if (type !== "object" && type !== "function" && type !== "symbol"){
// => 基本類型值
switch(type) {
case 'number':
context = new Number(context);
break;
case 'string':
context = new String(context);
break;
case 'boolean':
context = new Boolean(context);
break;
}
};
<!--必須保證context是引用類型-->
<!--this是call以前要執行的函數(obj.fn)-->
// 關鍵步驟
context.$fn = this;
let result = context.$fn(...args);
delete context.$fn;
return result;
}
proto.call = call;
function apply(context = window, args) {
context.$fn = this;
let result = context.$fn(...args);
delete context.$fn;
return result;
}
proto.apply = apply;
}(Function.prototype);
let obj = {
fn(x, y) {
console.log(this, x, y);
}
};
obj.fn.call(window,10,20); // Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …} 10 20
obj.fn.call(1,10,20); // Number {1, $fn: ƒ} 10 20
obj.fn.call(true,10,20); // Boolean {true, $fn: ƒ} 10 20
obj.fn.apply(true,[10,20]); // Boolean {true}__proto__: Boolean[[PrimitiveValue]]: true (2) [10, 20] undefined
複製代碼
function call(context = window, ...args) {
// 必須保證context是引用類型
context.$fn = this;
let result = context.$fn(...args);
delete context.$fn;
return result;
}
call 引用類型 堆地址AAAFFF000
複製代碼
function fn1() { console.log(1); }
function fn2() { console.log(2); }
fn1.call(fn2); // 執行的是fn1 => 1
fn1.call.call(fn2); // 最終讓fn2執行 => 2 (包括多個call)
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);
複製代碼
fn1.call.call(fn2);
一、先讓最後一個call執行,
最後一個call中的this是fn1.call,context是fn2
this => fn1.call => AAAFFF000
context => fn2
args => []
最後一個call開始執行
fn2.$fn = AAAFFF000
result = fn2.$fn(...[]) (AAAFFF000) 執行,
接着讓call第二次執行
this => fn2
context => undefined
args => []
undefined.$fn = fn2
result = undefined.$fn => (fn2())
最終讓fn2執行
複製代碼