好久不更新了哦,最近又在找工作。复习一下JS中非常重要的一部分内容,关于this指向的问题
常规的this指向可以简单总结为谁引用就指向谁,找不到谁引用就指向window。
this指向中还有非常重要的一部分,就是改变this指向的方法。原生JS提供了call、apply、bind三种方式来修改this指向,三种方式各有特点,各有应用场景。
在如今的前端面试中,会用call、apply、bind已经达不到要求了,要能够知道原理并手写实现。
先看一下原生call的使用方式。
function fn() {
console.log(this.name);
}
let obj = {
name: "微信公众号: Code程序人",
};
fn.call(obj);
我们先写了一个fn
函数,输出this.name
,在fn
函数中并没有绑定过this.name
,所以,如果不改变this
指向的话,它会输出undefined
。
所以我们用call
方法,改变一下this
指向。
可以看到改变this
指向后,在fn
函数中输入了它本身不存在的name
。
还可以添加额外的参数。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
fn.call(obj, 22, '男');
我们想让fn
函数额外接收两个参数。可以在call
方法的第二个参数开始依次填写要传入的参数。
我们知道了原生call
的使用方法后,着手自己实现一下。
Function.prototype.myCall = function (ctx) {
ctx = ctx || window;
let fn = Symbol();
ctx[fn] = this;
let args = [...arguments].splice(1);
let result = ctx[fn](...args);
delete ctx[fn];
return result;
};
因为我们在使用的时候,可以直接在函数的后面通过.call
的方式来使用,所以我们一定要在Function
原型链上绑定自己写的myCall
方法。
ctx
就是我们要this
要指向的对象。如果ctx
不存在的话,默认指向window
。
Symbol
如果不了解的话,可以去百度一下,它可以生成一个独一无二的值,一般用于对象的属性上。
ctx[fn] = this
,这句话,可以把要执行的函数绑定到ctx的属性上,之前说过this
指向的基本原则是谁调用就指向谁。比如我们执行fn.call()
,这个时候call
函数里的this
指向就是fn
。这样达到一个切换this
指向的作用。
要实现填入参数,要先了解一下arguments
对象,这是一个Function
中都存在的一个类数组的东西。它存入的是函数中的所有参数。
我们要的是从第二个参数开始的所有参数,都赋值给新函数中。
...arguments
可以将一个类数组转为一个数组,...
是ES6的扩展运算符,不懂的同学也可以去自行学习一下。
然后通过数组的splice
方法,删掉第一个参数,第一个参数是携带this
指向的对象,第二个参数开始才是传入函数需要的参数。
fn.call()
在改变this
指向后会默认执行这函数,所以执行函数的操作我们也要在函数中执行。
let result = ctx[fn](...args);
这段代码是执行函数的操作,并且args
就我们需要传入的参数,同样通过扩展运算符...
来展开。
执行完之后,其实有用的只有result
,我们要把之前辅助用的ctx[fn]
删掉,然后返回result
即可。有些有返回值,有些没有。都默认返回。
验证一下。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
// fn.call(obj, 22, '男');
fn.myCall(obj, 23, '男');
通过结果来看,我们写的myCall
函数与原生的call
函数实现了同样的功能。
apply
与call
最大的不同就是他们在传入参数时候,call
是将多余的函数参数从call
方法的第二个参数开始依次传入的。而apply
是将多余的函数参数放在一个数组里,从第二个参数中统一传入。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
fn.apply(obj, [24, '男']);
所以我们来尝试实现一下。
Function.prototype.myApply = function (ctx) {
ctx = ctx || window;
let fn = Symbol();
ctx[fn] = this;
let result;
if (arguments[1]) {
result = ctx[fn](...arguments[1]);
} else {
result = ctx[fn]();
}
delete ctx[fn];
return result;
};
实现的过程也和call
基本一致。只是在执行函数时,要判断一下,使用者有没有传入第二个参数,如果有的话,执行时传入...arguments[1]
就可以。如果没有传入,在执行时就不传入任何东西。
尝试使用一下。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
fn.myApply(obj, [25, '男']);
通过结果可以看到,我们成功通过自己的方式实现了apply
函数。
bind
与call
相比,在传入参数的方式上是一致的,bind
也是将需要传入的函数参数从第二个参数开始依次传入。
但是bind
与call
和apply
不同的点是,它在执行后,会返回一个修改this指向之后的函数,要手动去执行,而call
和apply
是自动执行。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
// fn.myApply(obj, [25, '男']);
console.log(fn.bind(obj, 26, '男'));
我们输出一下bind
之后的值。
function fn(age, sex) {
console.log(this.name, age, sex);
}
let obj = {
name: "微信公众号: Code程序人生",
};
// fn.call(obj, 22, '男');
// fn.myCall(obj, 23, '男');
// fn.apply(obj, [24, '男']);
// fn.myApply(obj, [25, '男']);
fn.bind(obj, 26, '男')();
知道原生bind
的使用方法之后,我们尝试实现一下。
Function.prototype.myBind = function(ctx) {
ctx = ctx || window;
let self = this;
let args = [...arguments].splice(1);
let fn = function() {};
let _fn = function() {
return self.apply(this instanceof _fn ? this : ctx, args);
}
fn.prototype = this.prototype;
_fn.prototype = new fn();
return _fn;
}
bind
的实现过程就稍微复杂了一些。我们需要在函数中,生成一个待执行且改变了this
指向的函数。
因为bind
和call
、apply
最主要的区别就是前者是返回改变this
指向后的函数,后者是自动执行。所以在函数中改变this
指向的操作就没必要重复写了,我这里使用现有的apply
来实现。
_fn
函数中判断this instanceof _fn
的目的是,原生bind
其实是可以new
那个bind
后返回的函数的,不是new
的情况下this
指向才会是ctx
。与call
和apply
不一样的地方是,这里还需要设置一下返回的那个函数的原型,采用继承的方式,不能直接 = ,要采用一个中介函数fn
来辅助改变_fn
的原型。
手写实现原生JS的各种方法是考察JS基本功的一种方式,我们首先要了解它的基本原理与使用方式,通过使用方式来一步步通过自己的方式尝试实现