【THE LAST TIME】this:call、apply、bind
前言
The last time, I have learned
【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
欢迎大家多多评论指点吐槽。

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见Nealyang/personalBlog。目录皆为暂定
讲道理,这篇文章有些拿捏不好尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,但是往往这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。
THE LAST TIME 系列
This
相信使用过 JavaScript 库做过开发的同学对 this 都不会陌生。虽然在开发中 this 是非常非常常见的,但是想真正吃透 this,其实还是有些不容易的。包括对于一些有经验的开发者来说,也都要驻足琢磨琢磨~ 包括想写清楚 this 呢,其实还得聊一聊 JavaScript 的作用域和词法
This 的误解一:this 指向他自己
function foo(num) {
console.log("foo:"+num);
this.count++;
}
foo.count = 0;
for(var i = 0;i<10;i++){
foo(i);
}
console.log(foo.count);
通过运行上面的代码我们可以看到,foo函数的确是被调用了十次,但是this.count似乎并没有加到foo.count上。也就是说,函数中的this.count并不是foo.count。
This 的误解二:this 指向他的作用域
另一种对this的误解是它不知怎么的指向函数的作用域,其实从某种意义上来说他是正确的,但是从另一种意义上来说,这的确是一种误解。
明确的说,this不会以任何方式指向函数的词法作用域,作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说他是对的,但是JavaScript代码不能访问这个作用域“对象”,因为它是引擎内部的实现
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
全局环境中的 This
既然是全局环境,我们当然需要去明确下宿主环境这个概念。简而言之,一门语言在运行的时候需要一个环境,而这个环境的就叫做宿主环境。对于 JavaScript 而言,宿主环境最为常见的就是 web 浏览器。
如上所说,我们也可以知道环境不是唯一的,也就是 JavaScript 代码不仅仅可以在浏览器中跑,也能在其他提供了宿主环境的程序里面跑。另一个最为常见的就是 Node 了,同样作为宿主环境,node 也有自己的 JavaScript 引擎:v8.
- 浏览器中,在全局范围内,
this等价于window对象 - 浏览器中,用
var声明一个变量等价于给this或者window添加属性 - 如果你在声明一个变量的时候没有使用
var或者let(ECMAScript 6),你就是在给全局的this添加或者改变属性值 - 在 node 环境里,如果使用
REPL来执行程序,那么this就等于global - 在 node 环境中,如果是执行一个 js 脚本,那么
this并不指向global而是module.exports为{} - 在node环境里,在全局范围内,如果你用
REPL执行一个脚本文件,用var声明一个变量并不会和在浏览器里面一样将这个变量添加给this - 如果你不是用REPL执行脚本文件,而是直接执行代码,结果和在浏览器里面是一样的
- 在
node环境里,用REPL运行脚本文件的时候,如果在声明变量的时候没有使用var或者let,这个变量会自动添加到global对象,但是不会自动添加给this对象。如果是直接执行代码,则会同时添加给global和this
这一块代码比较简单,我们不用码说话,改为用图说话吧!

函数、方法中的 This
很多文章中会将函数和方法区分开,但是我觉得。。。没必要啊,咱就看谁点了如花这位菇凉就行
当一个函数被调用的时候,会建立一个活动记录,也成为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。
函数中的 this 是多变的,但是规则是不变的。
你问这个函数:”老妹~ oh,不,函数!谁点的你?“
”是他!!!“
那么,this 就指向那个家伙!再学术化一些,所以!一般情况下!this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境。this 与声明无关!
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
记住上面说的,谁点的我!!! => foo() = windwo.foo(),所以其中this 执行的是 window 对象,自然而然的打印出来 2.
需要注意的是,对于严格模式来说,默认绑定全局对象是不合法的,this被置为undefined。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
虽然这位 xx 被点的多了。。。但是,我们只问点他的那个人,也就是 ojb2,所以 this.a输出的是 42.
注意,我这里的点!不是你想的那个点哦,是运行时~
构造函数中的 This
恩。。。这,就是从良了
还是如上文说到的,this,我们不看在哪定义,而是看运行时。所谓的构造函数,就是关键字new打头!
谁给我 new,我跟谁
其实内部完成了如下事情:
- 一个新的对象会被创建
- 这个新创建的对象会被接入原型链
- 这个新创建的对象会被设置为函数调用的this绑定
- 除非函数返回一个他自己的其他对象,这个被new调用的函数将自动返回一个新创建的对象
foo = "bar";
function testThis(){
this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行尝试
call、apply、bind 中的 this
恩。。。这就是被包了
在很多书中,call、apply、bind 被称之为 this 的强绑定。说白了,谁出力,我跟谁。那至于这三者的区别和实现以及原理呢,咱们下文说!
function dialogue () {
console.log (`I am ${this.heroName}`);
}
const hero = {
heroName: 'Batman',
};
dialogue.call(hero)//I am Batman
上面的dialogue.call(hero)等价于dialogue.apply(hero)``dialogue.bind(hero)().
其实也就是我明确的指定这个 this 是什么玩意儿!
箭头函数中的 this
箭头函数的 this 和 JavaScript 中的函数有些不同。箭头函数会永久地捕获 this值,阻止 apply或 call后续更改它。
let obj = {
name: "Nealyang",
func: (a,b) => {
console.log(this.name,a,b);
}
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); // 1 2
let func_ = func.bind(obj);
func_(1,2);// 1 2
func(1,2);// 1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);// 1 2
箭头函数内的 this值无法明确设置。此外,使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在创建时设置的 this值。
箭头函数也不能用作构造函数。因此,我们也不能在箭头函数内给 this设置属性。
class 中的 this
虽然 JavaScript 是否是一个面向对象的语言至今还存在一些争议。这里我们也不去争论。但是我们都知道,类,是 JavaScript 应用程序中非常重要的一个部分。
类通常包含一个 constructor , this可以指向任何新创建的对象。
不过在作为方法时,如果该方法作为普通函数被调用, this也可以指向任何其他值。与方法一样,类也可能失去对接收器的跟踪。
class Hero {
constructor(heroName) {
this.heroName = heroName;
}
dialogue() {
console.log(`I am ${this.heroName}`)
}
}
const batman = new Hero("Batman");
batman.dialogue();
构造函数里的 this指向新创建的 类实例。当我们调用 batman.dialogue()时, dialogue()作为方法被调用, batman是它的接收器。
但是如果我们将 dialogue()方法的引用存储起来,并稍后将其作为函数调用,我们会丢失该方法的接收器,此时 this参数指向 undefined 。
const say = batman.dialogue;
say();
出现错误的原因是JavaScript 类是隐式的运行在严格模式下的。我们是在没有任何自动绑定的情况下调用 say()函数的。要解决这个问题,我们需要手动使用 bind()将 dialogue()函数与 batman绑定在一起。
const say = batman.dialogue.bind(batman);
say();
this 的原理
咳咳,技术文章,咱们严肃点
我们都说,this指的是函数运行时所在的环境。但是为什么呢?

我们都知道,JavaScript 的一个对象的赋值是将地址赋值给变量的。引擎在读取变量的时候其实就是要了个地址然后再从原地址读出来对象。那么如果对象里属性也是引用类型的话(比如 function),当然也是如此!

而JavaScript 允许函数体内部,引用当前环境的其他变量,而这个变量是由运行环境提供的。由于函数又可以在不同的运行环境执行,所以需要个机制来给函数提供运行环境!而这个机制,也就是我们说到心在的 this。this的初衷也就是在函数内部使用,代指当前的运行环境。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2

obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行.
总结
- 函数是否在new中调用,如果是的话this绑定的是新创建的对象
var bar = new Foo();
- 函数是否通过call、apply或者其他硬性调用,如果是的话,this绑定的是指定的对象
var bar = foo.call(obj);
- 函数是否在某一个上下文对象中调用,如果是的话,this绑定的是那个上下文对象
var bar = obj.foo();
- 如果都不是的话,使用默认绑定,如果在严格模式下,就绑定到undefined,注意这里是方法里面的严格声明。否则绑定到全局对象
var bar = foo();
小试牛刀
var number = 2;
var obj = {
number: 4,
/*匿名函数自调*/
fn1: (function() {
var number;
this.number *= 2; //4
number = number * 2; //NaN
number = 3;
return function() {
var num = this.number;
this.number *= 2; //6
console.log(num);
number *= 3; //9
alert(number);
};
})(),
db2: function() {
this.number *= 2;
}
};
var fn1 = obj.fn1;
alert(number);
fn1();
obj.fn1();
alert(window.number);
alert(obj.number);
评论区留下你的答案吧~
call & applay
上文中已经提到了 call、apply和 bind,在 MDN 中定义的 apply 如下:
apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数
语法:
fun.apply(thisArg, [argsArray])
- thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
- argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容。
如上概念 apply 类似.区别就是 apply 和 call 传入的第二个参数类型不同。
call 的语法为:
fun.call(thisArg[, arg1[, arg2[, ...]]])
需要注意的是:
- 调用 call 的对象,必须是个函数 Function
- call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
- 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
apply 的语法为:
Function.apply(obj[,argArray])
需要注意的是:
- 它的调用者必须是函数 Function,并且只接收两个参数
- 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
记忆技巧:apply,a 开头,array,所以第二参数需要传递数据。
请问!什么是类数组?
核心理念
借!
对,就是借。举个栗子!我没有女朋友,周末。。。额,不,我没有摩托车
【THE LAST TIME】this:call、apply、bind的更多相关文章
- 菜鸟入门【ASP.NET Core】11:应用Jwtbearer Authentication、生成jwt token
准备工作 用VSCode新建webapi项目JwtAuthSample,并打开所在文件夹项目 dotnet new webapi --name JwtAuthSample 编辑JwtAuthSampl ...
- 菜鸟入门【ASP.NET Core】5:命令行配置、Json文件配置、Bind读取配置到C#实例、在Core Mvc中使用Options
命令行配置 我们通过vs2017创建一个控制台项目CommandLineSample 可以看到现在项目以来的是dotnet core framework 我们需要吧asp.net core引用进来 ...
- 转: 【Java并发编程】之三:线程挂起、恢复与终止的正确方法(含代码)
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17095733 挂起和恢复线程 Thread 的API中包含两个被淘汰的方法,它们用 ...
- 菜鸟入门【ASP.NET Core】8:Middleware管道介绍、自己动手构建RequestDelegate管道
中间件:是汇集到以处理请求和响应的一个应用程序管道的软件. 每个组件: 可以选择是否要将请求传递到管道中的下一个组件. 之前和之后调用管道中的下一个组件,可以执行工作. 使用请求委托来生成请求管道. ...
- 菜鸟入门【ASP.NET Core】7:WebHost的配置、 IHostEnvironment和 IApplicationLifetime介绍、dotnet watch run 和attach到进程调试
WebHost的配置 我们用vs2017新建一个空网站HelloCore 可以使用ConfigureAppConfiguration对配置进行更改,比如说添加jsonfile和commandline配 ...
- 【PyQt5 学习记录】005:QMainWindow 及状态栏、菜单栏和工具栏
#!/usr/bin/env python import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QA ...
- 【Java并发编程】之三:线程挂起、恢复与终止的正确方法
挂起和恢复线程 Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的.如果在不合适的时候挂起线程(比如,锁定共享资源时), ...
- 【深入Java虚拟机】之一:Java内存模型与内存溢出
[深入Java虚拟机]之:Java内存区域与内存溢出 高速缓存模型如下: ----------------------------------------------------分割线-------- ...
- 【深入Java虚拟机】之一:Java内存模型
[深入Java虚拟机]之:Java内存区域与内存溢出 内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域.Java虚拟机规范将JVM所管理的内存分为以下几个 ...
- 【ZooKeeper怎么玩】之一:为什么需要ZK
博客已经搬家,见[ZooKeeper怎么玩]之一:为什么需要ZK 学习新东西首先需要搞清楚为什么学它,这是符合我们的一个认知过程.<!--more-->#ZooKeeper是什么ZooKe ...
随机推荐
- Linux 安装二进制MySQL 及 破解MySQL密码
1.确保系统中有依赖的libaio 软件,如果没有: yum -y install libaio 2.解压二进制MySQL软件包 tar xf mysql-5.7.24-linux-glibc2.12 ...
- 一篇RPO漏洞挖掘文章翻译加深理解。
这是我第一次尝试翻译一篇漏洞挖掘文章,翻译它也是为了加深理解它.这是一篇很有意思的漏洞挖掘文章. 前几天在看fd的博客,偶然看到了这篇文章,虽然有点老了.但是思路真的牛皮.我决定花费时间和精力研究它们 ...
- DOM的预习
HTML DOM 将 HTML 文档视作树结构.这种结构被称为节点树: 一些常用的 HTML DOM 方法: getElementById(id) - 获取带有指定 id 的节点(元素) append ...
- android 定时提醒 - Notification
定时弹出 demo ,代码如下: 1.MainActivity.java public class MainActivity extends Activity implements OnClickLi ...
- Qt无边框窗体-模拟模态窗体抖动效果
目录 一.概述 二.效果展示 三.功能实现 四.相关文章 原文链接:Qt无边框窗体-模拟模态窗体抖动效果 一.概述 用Qt开发windows客户端界面确实是一大利器,兼顾性能的同时,速度相对来说也不错 ...
- App引流增长技术:Deeplink(深度链接)技术
移动互联网时代,信息的分享传播无疑是 App 引流增长的关键,与其花费大量精力和成本找渠道.硬推广,不如从细节下手,用最快最简便的方法实现 Deeplink(深度链接)技术,打破信息孤岛.缩短分享路径 ...
- 章节十六、10-TestNG报告和日志
一.在进行自动化的过程中,日志一般采用log4j 2进行日志记录,但TestNG自己本身也带有日志记录功能(reporter),它的好处在于日志中记录的内容都是testng自动生成的. package ...
- java数据结构——二叉树(BinaryTree)
前面我们已经学习了一些线性结构的数据结构和算法,接下来我们开始学习非线性结构的内容. 二叉树 前面显示增.删.查.遍历方法,完整代码在最后面. /** * 为什么我们要学习树结构. * 1.有序数组插 ...
- APP稳定性测试
APP稳定性测试-monkey测试 第一篇-App稳定性测试-Monkey(基本操作) 准备工作 1.首先下载好adb工具 2.使用数据线连接电脑,打开usb调试 3.使用win+R打开运行, ...
- opencv边缘检测
人眼怎么识别图像边缘? 比如有一幅图,图里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘.也就是像素的灰度值快速变化的地方. sobel算子 sobel算子是一个离散差分算子. 图像 ...