Libx

我不知道的JavaScript:this

字数统计: 2,587阅读时长: 10 min
2018/07/15 Share

this可以说是在JS中相当复杂甚至有时可以说是“邪乎”的一个机制,他带来了很多便利,但有时侯会让人感到有些崩溃👽

今天就来梳理下this的部分。

为什么要使用this?

首先来看一段代码:

function identify(){
return this.name.toUpperCase()
}
function speak(){
var greeting = "hello"+ identify.call(this);
console.log(greeting)
}
var me = {name:"Faker"}
var you = {name:"Uzi"}
identify(you) // Uzi
identify(me) //Faker
speak.call(me); // hello Faker
speak.call(you);// hello Uzi

这段代码在不同的上下文对象中重复使用了函数identify和speak,下面我们来看一下不使用this时候该怎么写:

function identify(context){
return context.name.toUpperCase()
}
function spaek(context){
var greeting = "hello"+ identify(context);
console.log(greeting)
}

显然,在不使用this的时候,需要显示的来传递上下文对象,this的引入使得对象引用传递的更加优雅。

对this的误解

this指向函数自身?

若只是根据字面意思来说的话似乎this的意思就是这样,虽然可以使用函数来存储一些内容,但是this的机制并没有这么简洁。

首先还是来看例子

function test(){
this.count++
}
test.count = 0;
for(var i = 0;i<5;i++){
test()
}
console.log(test.count) // 0

test.count并没有像我们所期望的那样输出5,而是输出了0,很显然这说明在函数执行的时候,this并没有指向Test函数,那么this到底指向哪里了呢?其实这里是指向的全局,在浏览器中即是window。当我们打印this.window的时候,你会发现他打印的又是NaN,这也不难解释,因为你并未初始化一个window下的count,undefined++自然就成了NaN。至于为什么在这里指向Window。我们稍后再谈

this指向函数的作用域?

其实我很长一段时间都是这么理解的。。👀因为我确实应用这个准则解释了一些问题。虽然之后发现解释得不是很合理。其实这样理解并不正确。这里其实混淆了一些概念。

这里可以指出:this在任何时候都不指向函数的词法作用域,在JS内部,作用确实和对象类似,可见的标识符都是它的属性,但是作用域对象并不能通过JS代码访问,它存在于引擎内部。

我们依然使用例子来说明问题:

function foo(){
var a =2;
this.bar();
}
function bar(){
console.log(this.a)
}
foo() //undefined

这里在书中说到调用this.bar()不能成功,但是在浏览器环境中是可以成功的。bar挂载到了window上。当然这不是重点,重点在于这个代码想要使用this.a和this.bar连接foo的内部作用域。然后按照作用域查找,但这是不能实现的。


以上,this既不指向函数自身,也不止像函数的词法作用域。实际上this是在函数调用时才发生的绑定,他指向哪里取决于函数在哪里调用。

绑定规则

默认绑定

function foo(){
console.log(this.a)
}
var a = 2;
foo()

首先我们应该知道声明在全局作用域中的变量就是全局对象的一个同名属性,他们实际上就是同一个东西,并不是复制得到的。

我们来分析下代码,通过分析,foo的调用位置是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。

如果使用严格模式的话,全局对象无法使用默认绑定,this将绑定到undefined

隐式绑定

依然还是看代码

function foo (){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() //2

首先注意到foo的声明方式,及其之后是如何被当作引用属性添加到obj中,但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象

然后,调用位置会使用obj上下文来引用函数,因此可以说,函数被调用的时候obj对象包含它。

当函数引用拥有上下文对象时,隐式绑定会把函数调用的this绑定到这个上下文对象,因为调用foo时this被绑定到obj所以this.a和obj.a相同。

对象属性引用链中只有最后一层会影响调用位置。
举个🎂

function foo(){
console.log(this.a)
}
var obj1 = {
a:1,
foo:foo
}
var obj2={
a:2,
obj1:obj1
}
obj2.obj1.foo()// 1
隐式绑定丢失

隐式绑定丢失就是被隐式绑定的函数会丢失绑定对象,然后会应用默认绑定·将this绑定到全局对象或者undifined(取决于是否是严格模式)。来看一个这里的错误用法:

function foo(){
console.log(this.a)
}
var obj = {
a:1,
foo:foo
}
var bar = obj.foo
var a = "2"
bar() // 2

这段代码尝试使用obj.foo将this的绑定传递到bar的调用中,但实际上bar依然是一个对函数foo的引用,应用了默认绑定。

另一种更加常见的绑定丢失发生在传入回调函数的时候:这里看一个例子

function foo(){
console.log(this.a)
}
function dofoo(fn){
fn()
}
var obj ={
a:2,
foo:foo
}
var a =1;
dofoo(obj.foo) //1

参数传递其实是一种隐式赋值,因此我们传入函数的时候也会被隐式复制,所以结果和上个例子相同。需要注意的是,如果把函数传入语言内置的函数结果也是相同

显示绑定

在之前分析隐式绑定的时候,我们总是会构造一个对象,通过对象中的一个属性来间接引用函数,从而把this隐式的绑定到这个对象上。
如果不是用这种方法该如何做呢?

这里要提到两个非常有用的函数callapply

这两方法第一个参数是一个对象,他们会把这个对象绑定到this接着在调用函数时指向这个this,因此可以直接指定this的绑定对象因此也称作显示绑定。

依然是一个简单的小例子来直观表示一下:

function foo(){
console.log(this.a)
}
var obj ={a:1}
foo.call(obj) //1

call将this绑定到了obj上。
硬绑定的典型应用场景一种就是创建一个包裹函数传入所有的参数并且返回接受到的所有值:

function foo(something ){
return this.a + something
}
var obj = {
a: 2
}
var bar = function (){
return foo.apply(obj,arguments)
}
var b = bar(3)
console.log(b) //5
// 我们可以稍加改动 创建一个可以重复使用的辅助函数
function bind (fn,obj){
return function (){
return fn.apply(obj,arguments);
}
}

硬绑定是一种非常常用的模式,所以ES5中也提供了Function.prototype.bind()

new绑定

熟悉JS的同志们可能知道JS中的构造函数其实和传统面向对象语言的有非常大的不同,如果你认为他的构造函数和传统的语言一样那你可能对JS有什么误解。

JS中的new使用方法是和传统的那些面向对象语言类似的,但是他们的机制完全不同。事实上,JS中的构造函数可以说只是一个使用new操作符时被调用的函数,它并不属于某个类,也不会实例化某个类,实际上,他都算不上一个特殊的函数类型,他只是碰巧被new调用了一下而已。并不存在所谓的”构造函数“,只存在”构造调用“

在执行new的调用时,会执行以下的操作:

  • 构建一个全新的对象、
  • 这个新对象会被执行原型连接
  • 这个新对象绑定到函数调用的this
  • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

我们这里暂时不关心第二步(之后也会详细梳理)

看个例子:

function foo(a){
this.a = a
}
var bar = new foo(2)
console.log(bar.a) //2
```
---
### 绑定方式优先级
默认绑定优先级自然是最低的,显示绑定也比隐式绑定的优先级要高,现在我们来分析下隐式绑定和new绑定的优先级级别
```javascript
function foo(some){
this.a = some
}
var obj1 = {foo:foo}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a) //2
obj1.foo.call(obj2,3)
console.log(obj2.a) //3
var bar = new obj1.foo(4)
console.log(obj1.a) //2
console.log(bar.a) //4

可以看到new绑定的优先级高于隐式绑定

接下来我们来讨论一下new绑定和显示绑定的优先级问题,之前提到过,bind会创建一个新的函数这个函数会忽略他当前的this绑定(无论当前绑定对象是什么)并把提供的对象绑定到this上。但是事实上并不是完全这样。

function foo(data){
this.a = data
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2) //obj1.a = 2
var baz = new bar(1)
console.log(obj1.a) //2
console.log(baz.a) //1

可以看到bar被硬绑定到obj1上,但是new bar(1)并没有把obj1.a也修改为1而是使用了new绑定,得到了一个一个名字为baz的新对象。

似乎之前所写到的bind并没有提供改变this指向的功能,事实上bind函数所提供的实现当然要复杂的多,简单的说代码会判断硬绑定函数是否会被new调用,如果是的话就会用新创建的this替换绑定的this

()=>{}中的this

箭头函数的this不使用4种绑定规则,而是根据外层(函数或者全局)作用域来决定this,定义时候绑定

function foo (){
return (a)=>{
console.log(this.a)
}
}
var obj1 ={ a:1}
var obj2 ={ a:2}
var bar = foo.call(obj1)
bar.call(obj2) //1

foo内部的箭头函数会捕获调用时foo的this,由于foo的this绑定到obj1 ,bar的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)

ES6中定义的时候绑定this的具体含义,应该继承的是父执行上下文里面的this,不是父执行上下文

简单来说类似于

function foo (){
var self = this
return function(a){
console.log(self.a)
}
}
var obj1 ={ a:1}
var obj2 ={ a:2}
var bar = foo.call(obj1)
bar.call(obj2) //1


暂时写到这里
2018.7.16/16:43

CATALOG
  1. 1. 为什么要使用this?
  2. 2. 对this的误解
    1. 2.1. this指向函数自身?
    2. 2.2. this指向函数的作用域?
  3. 3. 绑定规则
    1. 3.1. 默认绑定
    2. 3.2. 隐式绑定
      1. 3.2.1. 隐式绑定丢失
    3. 3.3. 显示绑定
    4. 3.4. new绑定
  4. 4. ()=>{}中的this