题外篇
如何改变this
得指向,常见的四种操作如下
- 使用
call、apply、bind
- 在执行函数内部使用
let that = this
-
es6
中使用箭头函数 - 对象实例化
new
操作
关于this
的指向
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
参考来自 波波老师
call
根据 MDN 的解释:
call()
方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。
语法:
fun.call(thisArg, arg1, arg2, ...)
thisArg:fun
函数运行时指定得this
值
this
得值可能有如下几种可能:
- 在严格模式下,传
null,undefined
or 不传,this
默认指向window
对象 - 传其他函数的函数名称,如
fn,this
指向fn
函数 - 传其他对象,
this
指向这个对象
看个例子
let obj = {
val:'call'
}
function fn () {
console.log(this.val,'testCall');
}
fn.call(obj) //'call','testCall'
模拟第一版
先考虑可以正常执行,后面的传参暂时不考虑
琢磨一下上面例子的代码执行过程
call()
在执行过程中,我们想象一下它大概会经历一些几个阶段(真实原理不做介绍)
- 将
fn
方法复制到obj
对象中 - 改变
fn
函数的this
指向 - 将
fn
函数执行 - 把
fn
从obj
对象删除
分析:
那么我们在模拟代码的场景下
fn.call(obj)
的执行过程可以想象成如下步骤:
1、将fn复制到obj对象中,那么如下也就修改了fn中this的指向
obj = {
val:'call',
fn:function(){
console.log(this.val,'testCall')
}
2、 执行fn()
obj.fn()
3、删除fn这个key
delete obj.fn
模拟开始
Function.prototype.call2 = function(args){
//此时的args就是 上面的obj
//1,此时使用this来获取调用call的方法
args.fn = this;
//第二步 调用执行fn()
args.fn();
//第三步 删除方法
delete args.fn
}
//测试下
let obj = {
val:'call2'
}
function fn () {
console.log(this.val,'testCall2');
}
fn.call2(obj) //'call2','testCall2'
模拟二版
MDN
文档上介绍过,call
可以接受多个参数,那么在第二版的时候我们加上入参这个功能
栗子
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
}
fn.call(obj,'alan') //'call','alan'
分析:
- 跟第一阶段相比就是多了一个传参,有疑惑的地方,可能不知道穿几个参数,不慌,可以从
Arguments
中获取第二个开始到最后结束的参数就行了
模拟开始
// 第二版
Function.prototype.call2 = function(...args) {
//利用es6的 rest 来获取函数的传参,以及传入thisArg;
let [thisArg,...arr] = args ;
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
thisArg.fn(...arr)
//删除
delete thisArg.fn
}
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
}
fn.call2(obj,'alan') //'call','alan'
解释:
-
es6
的 rest (形式为...变量名),这样可以得到一个数组,即args
此时为数组,那么上文中的thisArg
就是传递的第一个参数。 -
fn(..arr)
使用了es6
spread ,他就好比是reset
的逆运算,这样操作以后不管传递了几个参数都可以正常处理
模拟第三版
文章开头介绍过,如果在严格模式下,传
null,undefined
or 不传,thisArg
默认指向window
对象,还有一种场景如果fn
方法有返回值的情况。
栗子 1
var val = 'call'
function fn () {
console.log(this.val);
}
fn.call() //'call'
fn.call(null);//'call'
fn.call(undefind);//'call'
分析:
如果不传值或传null
等值,处理起来不算麻烦,稍微在我们原来的版本上做一些修改就好,看如下代码
// 3.1
Function.prototype.call2 = function(...args) {
let thisArg,arr = [];
if(args.length === 0 || !args[0]){
thisArg = window;
} else{
//利用es6的解构来获取函数的传参,以及传入thisArg;
[thisArg,...arr] = args ;
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
thisArg.fn(...arr)
//删除
delete thisArg.fn
}
fn.call2() //'call'
fn.call2(null);//'call'
fn.call2(undefind);//'call'
栗子2
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.call(obj,'alan') //'call','alan'
//
{
val:'call',
name:'alan'
}
终极版本
// 3.2
Function.prototype.call2 = function(...args) {
let thisArg,arr = [];
if(args.length === 0 || !args[0]){
thisArg = window;
} else{
//利用es6的解构来获取函数的传参,以及传入thisArg;
[thisArg,...arr] = args ;
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
let result = thisArg.fn(...arr)
//删除
delete thisArg.fn
return result
}
let obj = {
val:'call'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.call2(obj,'alan') //'call','alan'
//
{
val:'call',
name:'alan'
}
apply
apply的实现方式跟call基本相似,就是在传参上,apply接受的是数组,直接就贴一下代码
Function.prototype.apply2 = function(thisArg,arr) {
if(!thisArg){
thisArg = window;
}
// 获取调用的函数方法
thisArg.fn = this;
// 用解构执行函数
let result = thisArg.fn(...arr)
//删除
delete thisArg.fn
return result
}
let obj = {
val:'apply'
}
function fn (name) {
console.log(this.val,name);
return {
val:this.val,
name:name
}
}
fn.apply2(obj,['alan']) //'apply','alan'
bind
根据 MDN 的解释:
bind()
方法创建一个新的函数,在调用时设置this
关键字为提供的值。将给定参数列表作为原函数的参数序列的前若干项。
语法:
fun.bind(thisArg,arg1,arg2......)
bind()
方法会创建一个新的函数,一般叫绑定函数可以接受参数,这个地方注意,它可以在
bind
的时候接受参数,同时bind()
返回的新函数也可以接受参数
栗子
var obj = {
val: 'bind'
};
function fn() {
console.log(this.val);
}
// 返回了一个函数
var bindObj = fn.bind(obj);
bindObj(); // bind
模拟第一版
照旧,暂时不考虑传参
分析:
-
bindObj()
的执行结果跟使用call
一样的,不同的是它需要调用返回的方法bindObj
琢磨上述代码执行过程,这个时候我们对比一下call
的模拟来看
-
bindObj
像是call
模拟过程中的fn
,而后bindObj()
就像是fn()
-
bind
返回的函数,我们可以想象成call()
调用只有返回的函数而不会执行,只是apply(),call()
是立即执行,而bind
需要再次调用执行
模拟开始
Function.prototype.bind2 = function (args) {
//通过this拿到调用方法
let that = this;
//使用一个闭包来存储call方法的结果
return function () {
return that.call(args);
}
}
var obj = {
val: 'bind'
};
function fn() {
console.log(this.val);
}
// 返回了一个函数
var bindObj = fn.bind2(obj);
bindObj(); // bind
模拟第二版
考虑下传参的场景,开头介绍过,传参有两种场景
栗子
let obj = {
val:'bind'
};
function fn(name,sex){
let o = {
val:this.val,
name:name,
sex:sex
}
console.log(o)
}
let bindObj = fn.bind(obj,'alan');
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
栗子分析:
- 首先
bind
的时候接受了一个参数name
,同时返回了一个函数 - 执行放回的函数的时候传入了第二个参数
sex
模拟分析:
- 首先考虑
bind
方法传参的场景,我们可以借用之前在call
函数中的方法,使用es6 rest
的方法。获取从第二个开始到结束的所有参数 - 考虑
bind
返回的函数传参,可以在写的时候,将bind
传参跟后续的传参合并
模拟开发
// 2.1
Function.prototype.bind2 = function (args) {
//通过this拿到调用方法
let that = this;
// 获取bind2函数从第二个参数到最后一个参数
let allArgs = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return that.apply(args, allArgs.concat(bindArgs));
}
}
//2.2 es6实现
Function.prototype.bind2 = function (...args) {
//利用es6的 rest 来获取函数的传参,以及传入thisArg;allArgs就是第二个参数到最后一个参数的数组
let [thisArg,...allArgs] = args ;
let that = this;
return function (...bindArgs) {
return that.apply(thisArg, allArgs.concat(bindArgs));
}
}
let obj = {
val:'bind'
};
function fn(name,sex){
let o = {
val:this.val,
name:name,
sex:sex
}
console.log(o)
}
let bindObj = fn.bind2(obj,'alan');
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
说明
-
Array.prototype.slice.call(arguments)
是如何将arguments
转换成数组的,首先调用call
之后,this
就指向了arguments
,或许我们可以假象一下slice
的内部实现是:创建一个新的数组,然后循环遍历this
,将this
的没一个值赋值给新的数组然后返回新数组。
结束语
大佬如果看到文中如有错误的地方欢迎指出支出,我会及时修改。
参考
http://es6.ruanyifeng.com/?search=spread&x=0&y=0