【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 ...
随机推荐
- 【3】Decision tree(决策树)
前言 Decision tree is one of the most popular classification tools 它用一个训练数据集学到一个映射,该映射以未知类别的新实例作为输入,输出 ...
- CodeBenchmark之压力测试详解
CodeBenchmark是一款高性能可视化的并发测试组件,通过组件可以对任意逻辑代码或服务进行并发测试:组件最终通过可视化的方式来显示测试结果,在测试结果中可以看到具体的并发情况和处理延时的分布.组 ...
- 通过Service访问应用 (1)
目录 通过Service访问应用 通过Pod IP访问应用 通过ClusterIP Service在集群内部访问 通过Service访问应用 通过之前的操作,应用部署完成了,我们的Demo网站已 ...
- 个人IP「Android大强哥」上线啦!
自从入职新公司之后就一直忙得不行,一边熟悉开发的流程,一边熟悉各种网站工具的使用,一边又在熟悉业务代码,好长时间都没有更文了. 不过新公司的 mentor(导师)还是很不错的,教给我很多东西,让我也能 ...
- JAVA截取后String字符串六位字符
public static void main(String[] args){ String cellphone="; String pwd = cellphone.substring(ce ...
- d010:盈数、亏数和完全数
题目: 对一个正整数N而言,将它除了本身以外所有的因子加起来的总和为S,如果S>N,则N为盈数,如果S<N,则N为亏数,而如果S=N,则N为完全数(Perfect Number).例如10 ...
- sql server 使用 partition by 分区函数 解决不连续数字查询问题
sql server表中的某一列数据为不一定连续的数字,但是需求上要求按照连续数字来分段显示,如:1,2,3,4,5,6,10,11,12,13, 会要求这样显示:1~6,10~13.下面介绍如何实现 ...
- 解决commBind: Cannot bind socket FD 18 to [::1]: (99) Cannot assign requested address squid
最近玩squid主要是为了爬虫代理,但是使用docker搭建squid的时候发现,docker一直默认使用的 ipv6,但是squid使用ipv4,导致无法绑定,出现commBind: Cannot ...
- Winform组合ComboBox和TreeView实现ComboTree
最近做Winform项目需要用到类似ComboBox的TreeView控件. 虽然各种第三方控件很多,但是存在各种版本不兼容问题.所以自己写了个简单的ComboTreeView控件. 下图是实现效果: ...
- git远程操作相关命令(remote 、push、fetch 、pull)
git remote 为了便于管理,Git要求每个远程主机都必须指定一个主机名.为了便于管理,Git要求每个远程主机都必须指定一个主机名. git remote[查看创库名] git remote 在 ...