01 前言
这又是一个面试经典问题,也是 ES5 中众多坑中的一个。虽然在 ES6 中可能会极大避免 this 产生的错误,但是为了一些老代码的维护,详细的了解一下 this 的指向和 call、apply、bind 三者的区别还是很有必要的。
02 this的指向
了解call,apply,bind之前我们先来了解下js中 this 的指向。其实 this 的指向,始终坚持一个原理: this 永远指向最后调用它的那个对象。
下面我们就来讨论下这些场隐式绑定的场景this指向:
- 全局上下文
- 直接调用函数
- 对象.方法的形式调用
- DOM事件绑定(特殊)
- new构造函数绑定
- 箭头函数
全局上下文
全局上下文默认this指向window, 严格模式下指向undefined。
直接调用函数
let obj = {
a: function() {
console.log(this);
}
}
let func = obj.a;
func();
这种直接调用函数的情况。this相当于全局上下文的情况,默认指向window。
对象.方法 调用
let obj = {
a: function() {
console.log(this);
}
}
obj.a();
这种对象.方法
的调用情况。this指向的是这个对象。
DOM事件绑定
<div></div>
<button onclick="myClick()">123</button>
let dom = document.querySelector('div');
dom.addEventerListener('click',function(){
console.log(this);
});
function myClick(){
console.log(this);
}
addEventerListener
中 this 默认指向绑定事件的元素。onclick
等系列事件和IE中使用的attachEvent
默认this指向window。
new构造函数
function Person(name) {
this.name = name;
this.age = 6;
}
let tz = new Person('Tz');
此时构造函数中的this指向实例对象(tz)。
箭头函数
箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。
let obj = {
a: function() {
let do = () => {
console.log(this);
}
do();
}
}
obj.a();
this找到最近的非箭头函数是a方法,a现在绑定着obj, 因此箭头函数中的this是obj。
03 apply,call,bind
bind,call,apply都是用于显性的绑定this,从而改变this的指向。
apply
MDN给出的定义是:
apply() 方法调用一个函数, 其具有一个指定的this值,以及作为 一个数组(或类似数组的对象) 提供的参数
语法:
func.apply(thisArg, [argsArray])
thisArg
必选。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。argsArray
可选。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(arr) {
Product.apply(this, arr); //this指向实例对象food
this.category = 'food';
}
let food = new Food(['cheese', 5])
call
MDN给出的定义是:
call() 方法使用一个指定的 this 值和单独给出的 一个或多个参数 来调用一个函数。
语法:
function.call(thisArg, arg1, arg2, …)
thisArg
可选。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。arg1, arg2, ...
指定的参数列表。
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price); //this指向实例对象food
this.category = 'food';
}
let food = new Food('cheese', 5);
bind
MDN给出的定义是:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
function.bind(thisArg[, arg1[, arg2[, …]]])
thisArg
调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。arg1, arg2, ...
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.bind(this, name, price)(); //this指向实例对象food
this.category = 'food';
}
let food = new Food('cheese', 5);
三者区别
- apply 使用 数组 作为参数;
- call 使用 参数列表 作为参数;
- bind 使用 参数列表 作为参数,且创建了一个新的函数,需要 被调用 才会起作用。
04 手写模拟实现apply方法
Function.prototype.myapply = function (thisArg, args) {
thisArg = Object(thisArg) || window;
let fn = Symbol();
thisArg[fn] = this;
let result = thisArg[fn](...args);
delete thisArg[fn];
return result;
}
//test
let obj = {
a: 1
}
function test(x1, x2) {
console.log(x1, x2) //2, 3
console.log(this.a) //1
}
test.myapply(obj, [2, 3])
05 手写模拟实现call方法
Function.prototype.mycall = function (thisArg, ...args) {
thisArg = Object(thisArg) || window;
let fn = Symbol();
thisArg[fn] = this;
let result = thisArg[fn](...args);
delete thisArg[fn];
return result;
}
//test
let obj = {
a: 1
}
function test(x1, x2) {
console.log(x1, x2) //2, 3
console.log(this.a) //1
}
test.mycall(obj, 2, 3)
06 手写模拟实现bind方法
Function.prototype.mybind = function(thisArg, ...args){
return (...newArgs) => {
return this.call(thisArg, ...args, ...newArgs)
}
}
// test
let obj = {
a: 1
}
function test(x1, x2) {
console.log(x1, x2) //2, 3
console.log(this.a) //1
}
test.mybind(obj, 2, 3)()