js学习笔记 --- this 详解

js中的this,如果没有深入的学习了解,那么this将会是让开发人员很头疼的问题。下面,我就针对this,来做一个学习笔记。

1.调用位置

在理解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的调用位置

2.绑定规则

2.1 默认绑定

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

function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
// 在本代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
// 如果使用严格模式,那么全局对象无法使用默认绑定,因此this会绑定到undefined。

2.2 隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。举例来说:

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

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

然而,调用位置会使用obj的上下文来引用函数,因此,可以说函数被调用时obj对象“拥有”或者“包含”它。

无论如何称呼这个模式,当foo()被调用时,落脚点确实指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。所以this.a和obj.a是一样的。

对象属性引用链中只有最后一层会影响调用位置。上代码:

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

2.2.1 隐式丢失

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

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

虽然bar是obj.foo 的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。
下面举一个回调函数中隐式丢失的例子:

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

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

2.3 显示绑定

在上面隐式绑定的时候,必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)的绑定到这个对象上。

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该如何处理?

基本上大部分函数会包含call(..)和apply(..)方法。但是有的时候JavaScript的宿主环境有时候会提供一些非常特殊的函数,可能没有这两个方法,但是极为罕见。

这两个函数的第一个参数是一个对象,会把这个对象绑定到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(...)是一样的,他们的区别体现在其他的参数上。

不过上述的代码不能很好地解决我们提出的丢失绑定的问题。

2.3.1 硬绑定

不过显示绑定的一个变种可以解决这个问题。
上代码:

function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);
}
var a = '123';
bar(); // 2
setTimeout(bar,10); // 2
bar.call(window); // 2 此时硬绑定的bar不能修改foo的this。foo总会在obj上调用。

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

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

2.3.2 API调用的“上下文”

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

function foo (item){
console.log(item,this.id);
}
var obj = {
id:"cool"
}; // 调用foo()时把this绑定到obj

[1,2,3].forEach(foo,obj);

// 1 cool 2 cool 3 cool

2.4 new绑定

js中使用new可以构造一个新的对象,使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

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

上代码:

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

使用new来调用foo()时,会构造一个新对象并绑定到foo()调用中的this上。

3.优先级。

  • 毫无疑问,默认绑定的优先级是最低的。
    那么隐式绑定和显示绑定谁更高?上代码:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

可以看到,显式绑定优先级更高,也就是说在判断时应当先考虑是否可以应用显式绑定。

  • new 绑定 VS 隐式绑定:
function foo(something) {
this.a = something;
}
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 绑定 VS 显示绑定:

new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来直接
进行测试。但是我们可以使用硬绑定来测试它俩的优先级。

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

可以看到,new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。

总结

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的
顺序来进行判断:

1.函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。

var bar = new foo()

2.函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象

var bar = foo.call(obj2)

3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。

var bar = obj1.foo()

4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。

var bar = foo()

4.箭头函数

之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

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

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体
现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经
在使用一种几乎和箭头函数完全一样的模式。

function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2

虽然 self = this 和箭头函数看起来都可以取代 bind(..),但是从本质上来说,它们想替
代的是 this 机制。

参考资料

  • 《你不知道的javaScript》---上卷

你好!我是 JHCan333,公众号:爱生活的前端狗的作者。公众号专注前端工程师方向,包括但不限于技术提高、职业规划、生活品质、个人理财等方面,会持续发布优质文章,从各个方面提升前端开发的幸福感。关注公众号,我们一起向前走!

JavaScript 中的 this 并不难的更多相关文章

  1. JavaScript中{}+{}

    在 JavaScript 中,加法的规则其实很简单,只有两种情况: 把数字和数字相加 把字符串和字符串相加 所有其他类型的值都会被自动转换成这两种类型的值. 为了能够弄明白这种隐式转换是如何进行的,我 ...

  2. 【转】JavaScript中,{}+{}等于多少?

    原文链接:http://www.2ality.com/2012/01/object-plus-object.html 译文链接:http://www.cnblogs.com/ziyunfei/arch ...

  3. 深入理解javascript 中的 delete(转)

    在这篇文章中作者从<JavaScript面向对象编程指南>一书中关于 delete 的错误讲起,详细讲述了关于 delete 操作的实现, 局限以及在不同浏览器和插件(这里指 firebu ...

  4. [译]JavaScript中,{}+{}等于多少?

    最近,Gary Bernhardt在一个简短的演讲视频“Wat”中指出了一个有趣的JavaScript怪癖:在把对象和数组混合相加时,会得到一些你意想不到的结果.本篇文章会依次讲解这些计算结果是如何得 ...

  5. Javascript中的delete

    一.问题的提出 我们先来看看下面几段代码,要注意的是,以下代码不要在浏览器的开发者工具(如FireBug.Chrome Developer tool)中运行,原因后面会说明: 为什么我们可以删除对象的 ...

  6. JavaScript中,{}+{}等于多少?

    最近,Gary Bernhardt 在一个简短的演讲视频“Wat”中指出了一个有趣的 JavaScript 怪癖: 在把对象和数组混合相加时,会得到一些意想不到的结果. 本篇文章会依次讲解这些计算结果 ...

  7. 详解javascript中this的工作原理

    在 JavaScript 中 this 常常指向方法调用的对象,但有些时候并不是这样的,本文将详细解读在不同的情况下 this 的指向. 一.指向 window: 在全局中使用 this,它将会指向全 ...

  8. 谈 JavaScript 中的强制类型转换 (2. 应用篇)

    这一部分内容是承接上一篇的, 建议先阅读谈 JavaScript 中的强制类型转换 (1. 基础篇) 前两章讨论了基本数据类型和基本包装类型的关系, 以及两个在类型转换中十分重要的方法: valueO ...

  9. Javascript中的delete介绍

    关于JavaScript中的Delete一直没有弄的很清楚,最近看到两篇这方面的文章,现对两文中部分内容进行翻译(内容有修改和添加,顺序不完全一致,有兴趣推荐看原文),希望能对大家有所帮助 一.问题的 ...

随机推荐

  1. 2018-2-13-win10-uwp-分治法

    title author date CreateTime categories win10 uwp 分治法 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 17:2 ...

  2. Mysql --09 Innodb核心特性——事务

    目录 Innodb核心特性--事务 1.什么是事务 2.事务的通俗理解 3.事务ACID特性 4.事务流程举例 5.事务的控制语句 6.事务隐式提交情况 7.事务日志redo基本功能 8.redo数据 ...

  3. linux安装jdk环境(多种方式)

    通过tar.gz压缩包安装 此方法适用于绝大部分的linux系统 1.先下载tar.gz的压缩包,这里使用官网下载. 进入: http://www.oracle.com/technetwork/jav ...

  4. WriteDataToFile(filename,pJsonData,strlen(pJsonData)+1)

    WriteDataToFile(filename,pJsonData,strlen(pJsonData)+1) 字节流的长度计算 发送的txt 文件是对的 zip exe出现字节计算错误 strlen ...

  5. Djano中static和media文件路径的设置

    对于常用的css.js.image和常用的工具类在django项目中要设置一个全局的路径,对所有的app都可以访问到这个路径下的文件 1在django项目的setting文件中设置对应的static和 ...

  6. LeetCode(力扣)——Search in Rotated Sorted Array2 搜索旋转排序数组 python实现

    题目描述: python实现 Search in Rotated Sorted Array2 搜索旋转排序数组   中文: 假设按照升序排序的数组在预先未知的某个点上进行了旋转. ( 例如,数组 [0 ...

  7. C++ 从txt文本中读取map

    由于存入文本文件的内容都为文本格式,所以在读取内容时需要将文本格式的内容遍历到map内存中,因此在读取时需要将文本进行切分(切分成key和value) 环境gcc #include<iostre ...

  8. Yii Ar model 查询

    Ar model 查询 参照表: CREATE TABLE tbl_user ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, username VA ...

  9. Xcode7.1环境下上架iOS App到AppStore 流程②

    前言部分 part二部分主要讲解 iOS App IDs 的创建.概要文件的配置.以及概要文件安装的过程. 一.iOS App IDs 的创建 1)进入如图1所示界面点击右上角箭头所指的加号 进入iO ...

  10. vue 项目的运行与 打包

    1.vue init webpack 2.npm install axios 3.npm run dev  运行项目 4.npm run build 打包项目 会生成一个dist 文件夹,我们只需要把 ...