JavaScript this绑定过程深入详解


Posted in Javascript onDecember 07, 2018

本文实例形式详细分析了JavaScript this绑定过程。分享给大家供大家参考,具体如下:

在理解this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个问题:这个this 到底引用的是什么?通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

下面我们来看看到底什么是调用栈和调用位置:

function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是baz -> bar
// 因此,当前调用位置在baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是baz -> bar -> foo
// 因此,当前调用位置在bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

注意我们是如何(从调用栈中)分析出真正的调用位置的,因为它决定了this 的绑定。

你可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所写的一样。但是这种方法非常麻烦并且容易出错。另一个查看调用栈的方法是使用浏览器的调试工具。绝大多数现代桌面浏览器都内置了开发者工具,其中包含JavaScript 调试器。就本例来说,你可以在工具中给foo() 函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条debugger;语句。运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。因此,如果你想要分析this 的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

绑定规则

我们来看看在函数的执行过程中调用位置如何决定this 的绑定对象。你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条。

1.默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

思考一下下面的代码:

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

你应该注意到的第一件事是,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。

接下来我们可以看到当调用foo() 时,this.a 被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this 的默认绑定,因此this 指向全局对象。

那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo() 是如何调用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this 会绑定到undefined:

function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

这里有一个微妙但是非常重要的细节,虽然this 的绑定规则完全取决于调用位置,但是只有foo() 运行在非strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与foo()的调用位置无关:

function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

通常来说你不应该在代码中混合使用strict mode 和non-strict mode。整个程序要么严格要么非严格。然而,有时候你可能会用到第三方库,其严格程度和你的代码有所不同,因此一定要注意这类兼容性细节。

2.隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。

思考下面的代码:

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

首先需要注意的是foo() 的声明方式,及其之后是如何被当作引用属性添加到obj 中的。但是无论是直接在obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。然而,调用位置会使用obj 上下文来引用函数,因此你可以说函数被调用时obj 对象“拥有”或者“包含”它。无论你如何称呼这个模式,当foo() 被调用时,它的落脚点确实指向obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this 绑定到这个上下文对象。因为调用foo() 时this 被绑定到obj,因此this.aobj.a 是一样的。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:

function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失

一个最常见的this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this 绑定到全局对象或者undefined 上,取决于是否是严格模式。

思考下面的代码:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然bar 是obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
JavaScript 环境中内置的setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待delay 毫秒
fn(); // <-- 调用位置!
}

就像我们看到的那样,回调函数丢失this 绑定是非常常见的。除此之外,还有一种情况this 的行为会出乎我们意料:调用回调函数的函数可能会修改this。在一些流行的JavaScript 库中事件处理器常会把回调函数的this 强制绑定到触发事件的DOM 元素上。

这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。

无论是哪种情况,this 的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置。之后我们会介绍如何通过固定this 来修复(这里是双关,“修复”和“固定”的英语单词都是fixing)这个问题。

3.显式绑定

就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this 间接(隐式)绑定到这个对象上。那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?JavaScript 中的“所有”函数都有一些有用的特性(这和它们的[[ 原型]] 有关——之后我们会详细介绍原型),可以用来解决这个问题。具体点说,可以使用函数的call(..) 和apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..) 和apply(..) 方法。这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。因为你可以直接指定this 的绑定对象,因此我们称之为显式绑定。

思考下面的代码:

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

通过foo.call(..),我们可以在调用foo 时强制把它的this 绑定到obj 上。如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this 的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)new Boolean(..) 或者new Number(..))。这通常被称为“装箱”。从this 绑定的角度来说,call(..)apply(..) 是一样的,它们的区别体现在其他的参数上,但是现在我们不用考虑这些。可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

1. 硬绑定

但是显式绑定的一个变种可以解决这个问题。

思考下面的代码:

function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的bar 不可能再修改它的this
bar.call( window ); // 2

我们来看看这个变种到底是怎样工作的。我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo 的this 绑定到了obj。无论之后如何调用函数bar,它总会手动在obj 上调用foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

另一种使用方法是创建一个i 可以重复使用的辅助函数:

function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
};
}
var obj = {
a:2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

由于硬绑定是一种非常常用的模式,所以在ES5 中提供了内置的方法Function.prototype.bind,它的用法如下:

function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

bind(..) 会返回一个硬编码的新函数,它会把参数设置为this 的上下文并调用原始函数。

2. API调用的“上下文”

第三方库的许多函数,以及JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..) 一样,确保你的回调函数使用指定的this。

举例来说:

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

这些函数实际上就是通过call(..) 或者apply(..) 实现了显式绑定,这样你可以少些一些代码。

4.new绑定

这是第四条也是最后一条this 的绑定规则,在讲解它之前我们首先需要澄清一个非常常见的关于JavaScript 中函数和对象的误解。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new 初始化类时会调用类中的构造函数。通常的形式是这样的:

something = new MyClass(..);

JavaScript 也有一个new 操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为JavaScript 中new 的机制也和那些语言一样。然而JavaScript 中new 的机制实际上和面向类的语言完全不同。

首先我们重新定义一下JavaScript 中的“构造函数”。在JavaScript 中,构造函数只是一些使用new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new 操作符调用的普通函数而已。

举例来说,思考一下Number(..) 作为构造函数时的行为,ES5.1 中这样描述它:

Number 构造函数

当Number 在new 表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。

所以,包括内置对象函数(比如Number(..))在内的所有函数都可以用new 来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行[[ 原型]] 连接。
3. 这个新对象会绑定到函数调用的this。
4. 如果函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。

思考下面的代码:

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

使用new 来调用foo(..) 时,我们会构造一个新对象并把它绑定到foo(..) 调用中的this上。new 是最后一种可以影响函数调用时this 绑定行为的方法,我们称之为new 绑定。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
探讨JavaScript中声明全局变量三种方式的异同
Dec 03 Javascript
js字符串转换成数字与数字转换成字符串的实现方法
Jan 08 Javascript
指定区域的图片自动按比例缩小的js代码(防止页面被图片撑破)
Feb 21 Javascript
jquery带动画效果幻灯片特效代码
Aug 27 Javascript
javascript原生ajax写法分享
Apr 10 Javascript
JQuery和html+css实现带小圆点和左右按钮的轮播图实例
Jul 22 jQuery
Bootstrap datepicker日期选择器插件使用详解
Jul 26 Javascript
微信小程序之电影影评小程序制作代码
Aug 03 Javascript
JavaScript定义函数的三种实现方法
Sep 23 Javascript
Vue组件库发布到npm详解
Feb 17 Javascript
原生JavaScript实现幻灯片效果
Feb 19 Javascript
k8s node节点重新加入master集群的实现
Feb 22 Javascript
React 使用recharts实现散点地图的示例代码
Dec 07 #Javascript
如何手动实现es5中的bind方法详解
Dec 07 #Javascript
JavaScript时间与时间戳的转换操作实例分析
Dec 07 #Javascript
vue+SSM实现验证码功能
Dec 07 #Javascript
详解如何为你的angular app构建一个第三方库
Dec 07 #Javascript
JS实现随机生成10个手机号的方法示例
Dec 07 #Javascript
angular学习之动态创建表单的方法
Dec 07 #Javascript
You might like
PHP输出英文时间日期的安全方法(RFC 1123格式)
2014/06/13 PHP
Linux下PHP安装mcrypt扩展模块笔记
2014/09/10 PHP
php+mysqli使用面向对象方式查询数据库实例
2015/01/29 PHP
PHP递归遍历文件夹去除注释并压缩php源代码的方法示例
2018/05/23 PHP
实现超用户体验 table排序javascript实现代码
2009/06/22 Javascript
event对象的方法 兼容多浏览器
2009/06/27 Javascript
鼠标划过实现延迟加载并隐藏层的js代码
2013/10/11 Javascript
js opener的使用详解
2014/01/11 Javascript
jQuery实现购物车计算价格功能的方法
2015/03/25 Javascript
js实现防止被iframe的方法
2015/07/03 Javascript
jQuery中$this和$(this)的区别介绍(一看就懂)
2015/07/06 Javascript
jQuery实现点击后标记当前菜单位置(背景高亮菜单)效果
2015/08/22 Javascript
非常酷炫的Bootstrap图片轮播动画
2016/05/27 Javascript
jQuery实现的无限级下拉菜单功能示例
2016/09/12 Javascript
微信js-sdk分享功能接口常用逻辑封装示例
2016/10/13 Javascript
react router4+redux实现路由权限控制的方法
2018/05/03 Javascript
vue中的自定义分页插件组件的示例
2018/08/18 Javascript
微信小程序云开发如何使用npm安装依赖
2019/05/18 Javascript
Vue 图片压缩并上传至服务器功能
2020/01/15 Javascript
[01:14]TI珍贵瞬间系列(六):冠军
2020/08/30 DOTA
Python3 入门教程 简单但比较不错
2009/11/29 Python
python使用Pycharm创建一个Django项目
2018/03/05 Python
pandas通过loc生成新的列方法
2018/11/28 Python
Python实现随机创建电话号码的方法示例
2018/12/07 Python
Python3.8中使用f-strings调试
2019/05/22 Python
python程序变成软件的实操方法
2019/06/24 Python
python获取array中指定元素的示例
2019/11/26 Python
python通用读取vcf文件的类(复制粘贴即可用)
2020/02/29 Python
python+opencv边缘提取与各函数参数解析
2020/03/09 Python
使用python创建生成动态链接库dll的方法
2020/05/09 Python
python中pandas库中DataFrame对行和列的操作使用方法示例
2020/06/14 Python
HTML中fieldset标签概述及使用方法
2013/02/01 HTML / CSS
医学院学生的自我评价分享
2013/11/19 职场文书
卫生巾广告词
2014/03/18 职场文书
关于Vue Router的10条高级技巧总结
2021/05/06 Vue.js
python使用PySimpleGUI设置进度条及控件使用
2021/06/10 Python