面试的重点难点的坑来啦!~/(ㄒ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执行
复制代码