Javascript深入浅出之this

这篇文章其实几天前就想写了,但是近日又看到了 this 的很多用法,诸如硬绑定、软绑定,都没用到过。所以不禁感慨,学无止境啊。 this 根据情境不同,指向性差异很大,所以今天就是进行一个简单的梳理,并且对其进行一些原理性的分析。

前言

关于this的误解有很多,有人认为 this 指向函数自身,有人认为 this 指向函数上一级,诸如此类的太多了,甚至就连《高程》中对其都有一个不太严谨的描述:

在全局函数中, this 等于 window ,而当函数被作为某个对象的方法调用时,this 等于那个对象。

今天我们就来探讨一下this的正确打开方式。

What’s this

前一篇文章中提到过, this 是执行上下文的一个属性,它不是一个变量,所以我们不能对其进行赋值操作,否则会报错;同时 this 属性是在函数调用时才确立的,它与函数如何定义没有任何关系,它只能决定于它的调用方式。

那今天的重点就是讨论几种调用方式了?当然不是,重点就是了解一下 this 是如何来寻找指向对象的,这里就要引出我们的 引用类型 了。

引用类型

引用类型有两种情况:标识符和属性访问器。在处理引用类型的时候,会返回一个引用类型值,我们用伪代码来表示一下:

1
2
3
4
5
6
7
8
var reference = {
// 拥有这个属性的对象
base: <base Object>,
// 引用名
referenceName: <reference name>,
// 严格模式标记(boolean) 后面省略这个属性(不太用得到)
strictReference: <strict flag>
}

举个栗子:

1
2
3
4
var obj = {
bar: function () {}
};
function foo() {}

在调用的中间结果中,返回了如下的引用类型的值:

1
2
3
4
5
6
7
8
var barReference = {
base: obj
referenceName: bar
};
var fooReference = {
base: global,
referenceName: foo
};

到了这里,准备工作已经做好了,下面就是 this关联规则 粉墨登场了。

在一个函数上下文中,先判断调用括号 () 的左边是否为引用类型, 如果不是则将 this 设置为 undefined , 如果是,再判断引用类型是否为属性引用,如果不是则将 this 设置为 undefined ,如果是,则将 this 设为引用类型值的 base 对象。如果 this 的值为 undefined ,其值会被隐式转换为全局对象;而如果在严格模式下,undefined 不可以被隐式的转换

下面我们就要通过具体的几个情景来看一下关联规则是如何具体运作的。

独立函数调用

独立函数的调用我这里将它分为独立全局函数调用和独立内部函数调用,什么叫做独立呢,就是直接调用,不显式依托对象。

独立全局函数调用

先看看一个简单的例子:

1
2
3
4
function foo() {
console.log(this);
}
foo(); // window (global对象无法直接访问)

让我们来分析一下上面的代码产生了什么引用类型值:

1
2
3
4
var fooReference = {
base: global,
referenceName: foo
};

如果在非严格模式下, this 会指向全局对象( global 对象无法直接访问,但是 globalwindow 属性引用了 global ,所以上面会输出 window )。但是在严格模式下, this 值将被设为 undefined

1
2
3
4
5
6
function foo() {
"use strict";
console.log(this);
}
foo(); // undefined

严格模式与函数调用位置无关,只与函数定义有关。
当然,如果你是全局使用严格模式,就不必考虑它了。

1
2
3
4
5
6
7
8
9
function foo() {
console.log(this);
}
(function(){
"use strict";
foo(); // window
});

独立内部函数调用

内部函数就和全局函数虽然名字很近似,但他们却有本质上的区别。哈哈,这里就不卖关子了,先来看代码:

1
2
3
4
5
6
7
8
9
function foo() {
function bar() {
console.log(this);
}
bar();
}
foo(); // window

这里呢, foo()bar() 最终输出 window ,这是为什么呢!!!

是这样的,我们来看一下上面代码产生的引用类型的值:

1
2
3
4
var barReference = {
base: AO(fooReference),
referenceName: bar
};

这里的 base 被绑定了 foo 函数活动对象,可是函数调用并不是属性调用,所以根据上面的规则, this 被赋值为 undefined ,并隐式地转换为全局对象。

关于引用类型的base对象以及属性名是怎么寻找的,以及什么时候寻找的,将在作用域那一篇文章中进行介绍。

方法调用

我们拿个栗子边吃边聊:

1
2
3
4
5
6
7
var obj = {
foo: function() {
console.log(this);
}
};
obj.foo(); // obj

它产生的引用类型的值:

1
2
3
4
var fooReference = {
base: obj,
referenceName: foo
};

就近原则

对象属性引用链中只有最后一层(最靠近调用函数)会影响调用位置。举例来说:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

绑定丢失

有一种情况我们需要考虑一下,就是进行传值的时候;

1
2
3
4
5
6
7
8
var obj = {
foo: function() {
console.log(this);
}
}
var bar = obj.foo;
bar(); // window

我们可以看到 bar() 输出的值变了,因为在进行调用的时候, bar 生成了一个新的引用类型值:

1
2
3
4
var barReference = {
base: global,
referenceName: bar
};

难道这就没了吗?当然不!我们引出这个概念,必须得有注意事项啊!

当我们把对象属性函数引用传递给函数或者是内置函数当做参数时,回想一下我们之前提到过的,它们会发生一个隐式的赋值传递,所以,这个时候就会发生绑定丢失。

1
2
3
4
5
6
7
8
9
10
11
12
function bar(m) {
// 这里发生了一个隐式的赋值,m = obj.foo;
m();
}
var obj = {
foo: function() {
console.log(this);
}
}
bar(obj.foo); // window

call、apply调用

.call().apply() 调用比较类似,它们的第一个参数都是传递给 this 的值,不同的是, .call() 后面的参数可以是任意值, .apply()第二个参数为传入值组成的数组(只有两个参数)。

老板,再来一个栗子:

1
2
3
4
5
6
7
8
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); // 2

通过 foo.call(...) ,我们可以在调用 foo() 时将 this 强制绑定到 obj 上。

bind调用

.bind() 是ECMAScript5中在函数原型上添加的方法,它将参数传递给this,并返回新的硬编码(函数内部显式绑定this)函数。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this.a);
}
var obj = {
a: 1
}
var bar = foo.bind(obj);
bar(); // 1

API调用

第三方库以及许多javascript中内置函数,会提供一个可选参数,成为“上下文”,确保你的回调函数使用指定的 this

For example:

1
2
3
4
5
6
7
8
9
function foo(el) {
console.log(el, this.id);
}
var obj = {
id: "awesome"
};
// 调用foo(..)时把this绑定到obj
[1, 2, 3].forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome

构造函数

大家都在说作为构造器调用的函数中的 this 都被设置为了新创建的对象,轻飘飘的一句话,我也能记住,可是究其原理呢?

我们来看一下ECMA5中对new过程的解释吧:(主要是ECMA6整的太复杂了)

  1. Let ref be the result of evaluating MemberExpression.
  2. Let constructor be GetValue(ref).
  3. Let argList be the result of evaluating Arguments, producing an internal list of argument values.
  4. If Type(constructor) is not Object, throw a TypeError exception.
  5. If constructor does not implement the [[Construct]] internal method, throw a TypeError exception.
  6. Return the result of calling the [[Construct]] internal method on constructor, providing the list argList as the argument values.

这是针对有参构造函数的,如果是无参构造函数,则省去第三步。
其大概意思是 ‘ new ‘ 后面必须跟一个对象并且该对象必须有一个名为 ‘ [[Construct]] ‘ 的内部方法,否则抛出异常。如果以上条件都成立,就执行 ‘ [[Construct]] ‘ 方法。

我们再来看看 [[Construct]] 内部方法是如何实现的:

  1. Let obj be a newly created native ECMAScript object. // 新建原生 js 对象 obj
  2. Set all the internal methods of obj as specified in 8.12. // 把 obj 的内置方法都设置好,它们都在8.12中罗列出来了
  3. Set the [[Class]] internal property of obj to “Object”. //把 obj 的 [[Class]] 内部属性设置为 ‘object’
  4. Set the [[Extensible]] internal property of obj to true. // 设置 [[Extensible]] 为 true
  5. Let proto be the value of calling the [[Get]] internal property of F with argument “prototype”. // 调用 [[get]] 方法得到构造函数 F 的 prototype 的值,赋给 proto
  6. If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto. // 如果 proto 是一个对象,将 obj 的 [[Prototype]] 设置为proto
  7. If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4. // 如果 proto 不是对象,则将 [[Prototype]] 设置为标准内置 Object 对象的 prototype 对象。
  8. Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args. // 调用构造函数 F 的内部 [[Call]] 方法并赋值给 result ,调用时提供 obj 作为 this 的值,将传入参数作为入口参数
  9. If Type(result) is Object then return result. // 如果 result 是对象,则返回 result 。
  10. Return obj. // result 不是对象则返回 obj 。

间接引用

当我们进行一些诸如赋值、 ||&& 之类的操作时,会不经意地创建一个“函数引用”。

1
2
3
4
5
6
7
var obj = {
foo: function () {
console.log(this);
}
};
(obj.foo = obj.foo)(); // window

这里输出window的原因就是因为 (obj.foo = obj.foo) 中存在 = , 它使得赋值前它右边的 obj.foo 调用了 getValue 方法, getValue 方法返回了 function 的引用,所以这里就等于独立函数调用了。

箭头函数

ECMAScript6中新添了一种箭头函数,它无法与上面四种情况混用,它的 this 取决于外层函数调用时绑定的 this ,而且它一旦被绑定无法更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
return (a) => {
console.log(this);
}
}
var obj1 = {
a: 1
};
var obj2 = {
a: 2
};
var bar = foo.bind(obj1);
var baz = bar.call(obj2);
baz(); // 1 不是 2,因为一旦绑定则无法更改

this 这一篇真的写了有一天了,有了很多感想,很多自己以为会了的东西其实根本就没有懂,因为写着写着就会发现自己有很多特别细节性的东西不值得推敲,甚至会有冲突,然后再逼着自己去找资料,看ECMA规范,这两天提高了很多吧。这是个好事也不是个好事,下次应当在写博文之前就整个地梳理遍,这样子知识才能更系统的消化,不至于显得写博客仅仅是来发现自己的不足。

热评文章