Skip to main content

造型百变的 this

· 7 min read

这里我们来聊一聊作用域, this 指向的区别,还有 手动绑定 this 指向的方法

什么是作用域

作用域我们在聊关键字var/let/const的时候也有说过, 作用域是用来指定变量/函数可供访问的范围,即变量/函数被定义和访问的范围及关系。

js是词法作用域,也叫做静态作用域,前面说过 js 有以下作用域

  • 全局作用域: window/global

  • 函数作用域

  • 块级作用域(ES6 -- let/const)

简单来说js的词法作用域就是在 js 进行语法词法分析的时候就确定的作用域 , 这里面就涉及到前面文章说过的变量提升等概念了

关键的 this

this 关键字在 js 编程中经常被使用,而且因为在不同场景下表现存在差异,很容易出现 bug , 所以我们来讨论下

首先我们先区分作用域和 this,简单来说如下

作用域在 js 语法分析的时候就已经确定 this 的指向是根据调用函数运行的上下文动态确定的

前面说到,this 的指向和调用函数存在关系,下面总结下几种关系

  • 在函数体中,简单调用函数,this 绑定到全局对象window/global,如果是在严格模式下,this 绑定到 undefined
  • 通过 new 调用的函数,this 会绑定到新创建的对象上
  • 根据上下文对象调用,绑定在该对象上
  • 通过 call/apply/bind 调用,绑定到指定参数的对象上
  • 如果是箭头函数,则会根据外层上下文绑定的 this 决定 this 指向

总结下, 除了箭头函数之外,其他情况下与调用方式相关, 而箭头函数则与完成的上下文绑定的 this 相关,看以下例子


const o1 = {
t1:'text',
fn1:function() {
setTimeout((){
console.log(this.t1);
});
}
}

o1.fn1(); // print undefined , this -> window

上面例子中 setTimeout 是匿名函数,this 指向 window

如果改成


const o1 = {
t1:'text',
fn1:function() {
setTimeout(() => {
console.log(this.t1);
});
}
}

o1.fn1(); // print text, this -> o1

setTimeout 匿名函数是箭头函数,箭头函数的 this 指向由上层 fn1的 this 绑定来确定

再看下面的问题

const o1 = {
t1:'text',
fn1:function() {
setTimeout(()=>{
console.log(this.t1);
},0);
}
}

const o2 = {
t1:'text2',
fn1: o1.fn1
}

o2.fn1(); // print text2 , this -> o2

首先 o2.fn1指向o1.fn1,而调用 fn1 是在 o2 ,因此 fn1 此时绑定的 thiso2, 而又如前面的箭头函数,因此 setTimeoutthis 指向 o2

call/apply/bind

前面我们讨论了 this 指向问题, 上面的都是根据规则被动绑定的 this 指向,那么如果我们要怎么进行主动的 this 指向绑定呢,这时候就会用到 callapplybind 这个方法来进行绑定, 这三个方法的关注点都是在函数this 指向 上面,从作用上面都是重新绑定函数的this 指向, 方法之间略微有些差别

call/apply

callapply 有点相似,都是直接执行对应的函数,通过传入的第一个参数指定函数运行时候的 this 指向,区别在与第一个参数后的参数, call 为逐个传入参数,apply 则是通过数组一次性传入参数

function foo(a,b,c,d){
console.log(this.tt);
console.log(a,b,c,d);
}

foo(1,2,3,4); // print undefined | 1,2,3,4 this-> window

foo.call({tt:123}, 1,2,3,4); // print 123 | 1,2,3,4 this -> {tt:123}

foo.apply({tt:123},[1,2,3,4]);// print 123 | 1,2,3,4 this -> {tt:123}

call/apply 性能上有什么区别

  • 根据 ecma 262 标准,里面描述的apply 执行步骤如下:

    图 2

    可以直接看step 3 和 6, 可以发现其实apply 是封装了call 的调用,因此理论上来说 call 的性能要比 apply 的好,不过在现代浏览器中,这个差别微乎其微;对于一些老旧的浏览器,有一个说法是如果你的参数列表长度小于3 ,则直接使用apply ,如果大于3 ,则将参数对应传入call 中,性能会更加好

bind

bind的作用也是通过第一个参数指定函数 this 的指向,与callapply不同的是,bind不直接运行函数,而是返回一个函数, 第一个参数后的参数会指定原函数对应函数的参数值, 返回的函数的参数会补充到原有参数列表的后面, 因此bind 也用于生成偏函数

function add(a,b,c){
console.log(this);
return a+b+c;
}

add(1,2,3); // print window | 6 this -> window

add1 = add.bind({a:1});
add1(1,2,3); // print {a:1} | 6 this -> {a:1}

add2 = add.bind({a:2}, 1,2);

add2(1,2,3); // print {a:2} | 4(1+2+1) this -> {a:2} 偏函数

bind/call/apply 混用

简单来说就是一个函数调用了bind生成了新函数,然后又用callapply进行调用,如果传入的作用域不一致,会以哪个为准,结论就是以 bind 的传入作为 this 的指向

function foo() {
console.log(this.a)
}
foo.bind({a:1}).call({a:2}); // print 1

相关问题

手写一个 bind