【进阶3-1期】JavaScript深入之史上最全--5种this绑定全面解析(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/20
this
的绑定规则总共有下面5种。
- 1、默认绑定(严格/非严格模式)
- 2、隐式绑定
- 3、显式绑定
- 4、new绑定
- 5、箭头函数绑定
现在开始一个一个介绍,内容来自《你不知道的JS》笔记整理。
1 调用位置
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
查找方法:
(1)、分析调用栈:调用位置就是当前正在执行的函数的前一个调用中
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)、使用开发者工具得到调用栈:
设置断点或者插入debugger;
语句,运行时调试器会在那个位置暂停,同时展示当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。
参考地址:https://www.cnblogs.com/lsgxeva/p/7976111.html
2 绑定规则
2.1 默认绑定
- 独立函数调用,可以把默认绑定看作是无法应用其他规则时的默认规则,this指向全局对象。
- 严格模式下,不能将全局对象用于默认绑定,this会绑定到
undefined
。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。
function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict"; console.log( this.a );
} var a = ; // 调用
foo(); // TypeError: Cannot read property 'a' of undefined // -------------------------------------- function foo() { // 运行
console.log( this.a );
} var a = ; (function() { // 严格模式下调用函数则不影响默认绑定
"use strict"; foo(); //
})();
2.2 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起作用。
function foo() {
console.log( this.a );
} var obj = {
a: ,
foo: foo
}; obj.foo(); //
隐式丢失
被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
// 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {
console.log( this.a );
} var obj = {
a: ,
foo: foo
}; var bar = obj.foo; // 函数别名 var a = "oops, global"; // a是全局对象的属性 bar(); // "oops, global"
参数传递就是一种隐式赋值,传入函数时也会被隐式赋值。回调函数丢失this绑定是非常常见的。
function foo() {
console.log( this.a );
} function doFoo(fn) {
// fn其实引用的是foo fn(); // <-- 调用位置!
} var obj = {
a: ,
foo: foo
}; var a = "oops, global"; // a是全局对象的属性 doFoo( obj.foo ); // "oops, global" // ---------------------------------------- // JS环境中内置的setTimeout()函数实现和下面的伪代码类似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}
2.3 显式绑定
通过call(..)
或者 apply(..)
方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。因为直接指定this的绑定对象,称之为显示绑定。
function foo() {
console.log( this.a );
} var obj = {
a:
}; foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上
显示绑定无法解决丢失绑定问题。
解决方案:
- 1、硬绑定
创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。这种方式让我想起了借用构造函数继承,没看过的可以点击查看 JavaScript常用八种继承方案
function foo() {
console.log( this.a );
} var obj = {
a:
}; var bar = function() {
foo.call( obj );
}; bar(); //
setTimeout( bar, ); // 2 // 硬绑定的bar不可能再修改它的this
bar.call( window ); //
硬绑定的缺点是:this绑定以后,就不能再修改了
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
function foo(something) {
console.log( this.a, something );
return this.a + something;
} var obj = {
a:
}; var bar = function() {
return foo.apply( obj, arguments );
}; var b = bar( ); // 2 3
console.log( b ); //
创建一个可以重复使用的辅助函数。
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:
}; var bar = bind( foo, obj ); var b = bar( ); // 2 3
console.log( b ); //
ES5内置了Function.prototype.bind
,bind会返回一个硬绑定的新函数,用法如下。
function foo(something) {
console.log( this.a, something );
return this.a + something;
} var obj = {
a:
}; var bar = foo.bind( obj ); var b = bar( ); // 2 3
console.log( b ); //
- 2、API调用的“上下文”
JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)
一样,确保回调函数使用指定的this。这些函数实际上通过call(..)
和apply(..)
实现了显式绑定。
function foo(el) {
console.log( el, this.id );
} var obj = {
id: "awesome"
} var myArray = [, , ]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
2.4 new绑定
- 在JS中,
构造函数
只是使用new
操作符时被调用的普通
函数,他们不属于某个类,也不会实例化一个类。 - 包括内置对象函数(比如
Number(..)
)在内的所有函数都可以用new
来调用,这种函数调用被称为构造函数调用。 - 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 1、创建(或者说构造)一个新对象。
- 2、这个新对象会被执行
[[Prototype]]
连接。 - 3、这个新对象会绑定到函数调用的
this
。 - 4、如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
使用new
来调用foo(..)
时,会构造一个新对象并把它(bar
)绑定到foo(..)
调用中的this。
function foo(a) {
this.a = a;
} var bar = new foo(); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); //
手写一个new实现
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
使用这个手写的new
function Person() {...} // 使用内置函数new
var person = new Person(...) // 使用手写的new,即create
var person = create(Person, ...)
代码原理解析:
1、用
new Object()
的方式新建了一个对象obj
2、取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以
arguments
会被去除第一个参数3、将
obj
的原型指向构造函数,这样obj
就可以访问到构造函数原型中的属性4、使用
apply
,改变构造函数this
的指向到新建的对象,这样obj
就可以访问到构造函数中的属性5、返回
obj
3 优先级
st=>start: Start
e=>end: End
cond1=>condition: new绑定
op1=>operation: this绑定新创建的对象,
var bar = new foo() cond2=>condition: 显示绑定
op2=>operation: this绑定指定的对象,
var bar = foo.call(obj2) cond3=>condition: 隐式绑定
op3=>operation: this绑定上下文对象,
var bar = obj1.foo() op4=>operation: 默认绑定
op5=>operation: 函数体严格模式下绑定到undefined,
否则绑定到全局对象,
var bar = foo() st->cond1
cond1(yes)->op1->e
cond1(no)->cond2
cond2(yes)->op2->e
cond2(no)->cond3
cond3(yes)->op3->e
cond3(no)->op4->op5->e
在new
中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new
进行初始化时就可以只传入其余的参数(柯里化)。(这个有点不懂)
function foo(p1, p2) {
this.val = p1 + p2;
} // 之所以使用null是因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new时this会被修改
var bar = foo.bind( null, "p1" ); var baz = new bar( "p2" ); baz.val; // p1p2
4 绑定例外
4.1 被忽略的this
把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认规则。
下面两种情况下会传入null
- 使用
apply(..)
来“展开”一个数组,并当作参数传入一个函数 bind(..)
可以对参数进行柯里化(预先设置一些参数)
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
} // 把数组”展开“成参数
foo.apply( null, [, ] ); // a:2,b:3 // 使用bind(..)进行柯里化
var bar = foo.bind( null, );
bar( ); // a:2,b:3
总是传入null
来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
更安全的this
安全的做法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何副作用。
JS中创建一个空对象最简单的方法是**Object.create(null)
**,这个和{}
很像,但是并不会创建Object.prototype
这个委托,所以比{}
更空。
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
} // 我们的空对象
var ø = Object.create( null ); // 把数组”展开“成参数
foo.apply( ø, [, ] ); // a:2,b:3 // 使用bind(..)进行柯里化
var bar = foo.bind( ø, );
bar( ); // a:2,b:3
如果我们想把this绑定到null或undefined,最好的办法是传入一个空对象(Object.ceate(null))
4.2 间接引用
间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
// p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo()
function foo() {
console.log( this.a );
} var a = ;
var o = { a: , foo: foo };
var p = { a: }; o.foo(); //
(p.foo = o.foo)(); //
4.3 软绑定
- 硬绑定可以把this强制绑定到指定的对象(
new
除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。 - 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有curried参数
var curried = [].slice.call( arguments, );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
[].slice.call(arguments, 1)
返回的是arguments数组从1号位开始的片段。
使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
function foo() {
console.log("name:" + this.name);
} var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" }; // 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj // 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!! // 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!! // 绑定丢失,应用软绑定
setTimeout( obj2.foo, ); // name: obj
5 this词法
ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this。
foo()
内部创建的箭头函数会捕获调用时foo()
的this。由于foo()
的this绑定到obj1
,bar
(引用箭头函数)的this也会绑定到obj1
,箭头函数的绑定无法被修改(new
也不行)。
function foo() {
// 返回一个箭头函数
return (a) => {
// this继承自foo()
console.log( this.a );
};
} var obj1 = {
a:
}; var obj2 = {
a:
} var bar = foo.call( obj1 );
bar.call( obj2 ); // 2,不是3!
ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。
function foo() {
var self = this; // lexical capture of this
setTimeout( function() {
console.log( self.a ); // self只是继承了foo()函数的this绑定
}, );
} var obj = {
a:
}; foo.call(obj); //
如果你经常编写this风格的代码,但是绝大多数都会使用self=this或者箭头函数来否定this机制,那你或许应当:
1、只使用词法作用域并完全抛弃错误this风格的代码
2、完全采用this风格,在必要时使用bind(..),尽量避免使用self=this和箭头函数
当然,包含这俩种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混合使用这俩种风格通常会使代码更难维护,并且可能也会更难编写
【进阶3-1期】JavaScript深入之史上最全--5种this绑定全面解析(转)的更多相关文章
- 暑假集训2016day3T1 欧拉回路(UOJ #117欧拉回路)(史上最全的欧拉回路纯无向图/有向图解析)
原题……可惜不会……真是一只大蒟蒻…… ———————————————————————————————— 有一天一位灵魂画师画了一张图,现在要你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好 ...
- [No00004F]史上最全Vim快捷键键位图(入门到进阶)
史上最全Vim快捷键键位重磅来袭!!学习Linux的朋友看过来啦,你是不是觉得Linux编辑器Vim操作复杂,步骤繁琐呢?Linux工程师是不是想大幅度提升自己的工作效率呢? 经典版 下 ...
- [No00004F]史上最全Vim快捷键键位图(入门到进阶)vim常用命令总结
在命令状态下对当前行用== (连按=两次), 或对多行用n==(n是自然数)表示自动缩进从当前行起的下面n行.你可以试试把代码缩进任意打乱再用n==排版,相当于一般IDE里的code format.使 ...
- 史上最全的Java高级技术点,全是Java高级进阶技术,几乎包含了Java后端的所有知识点
史上最全的Java高级技术点,全是Java高级进阶技术,几乎包含了Java后端的所有知识点 1
- 【Tips】史上最全H1B问题合辑——保持H1B身份终级篇
[Tips]史上最全H1B问题合辑——保持H1B身份终级篇 2015-04-10留学小助手留学小助手 留学小助手 微信号 liuxue_xiaozhushou 功能介绍 提供最真实全面的留学干货,帮您 ...
- 史上最全的CSS hack方式一览
做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...
- [转]史上最全的CSS hack方式一览
做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...
- GitHub上史上最全的Android开源项目分类汇总 (转)
GitHub上史上最全的Android开源项目分类汇总 标签: github android 开源 | 发表时间:2014-11-23 23:00 | 作者:u013149325 分享到: 出处:ht ...
- .Net魔法堂:史上最全的ActiveX开发教程——ActiveX与JS间交互篇
一.前言 经过上几篇的学习,现在我们已经掌握了ActiveX的整个开发过程,但要发挥ActiveX的真正威力,必须依靠JS.下面一起来学习吧! 二.JS调用ActiveX方法 只需在UserContr ...
随机推荐
- getaddrinfo函数
一.功能 对于IPv4和IPv6均适用,可以处理名字到地址以及服务到端口这两种变换,返回的是一个sockaddr结构而不是一个地址队列 二.函数原型 #include <netdb.h> ...
- select实现简单TCP通信(ubuntu 18.04)
一.服务器程序(server.c) #include <stdio.h> #include <unistd.h> #include <stdlib.h> #incl ...
- vs 调式连接oracle报错问题32,64位问题
wind8 系统选择项目时生成目标平台选择为X86 报错“System.Exception”类型的未经处理的异常在 WindowsFormsApplication1.exe 中发生 其他信息: 尝试加 ...
- Linux 文件删除 提示 Operation not permitted
Linux 删除 隐藏文件提示 Operation not permitted ? linux 删除 隐藏文件 提示 Operation not permitted 不允许操作? 使用 ls ...
- [C++]Linux之C编程异常[true未定义解决方案]
C语言里面是没有bool(布尔)类型的,C++里面才有,这就是说,在C++里面使用bool类型是没有问题的.bool类型有只有两个值:true =1 .false=0. 但是,C99标准里面,又定义了 ...
- Coursera Deep Learning 2 Improving Deep Neural Networks: Hyperparameter tuning, Regularization and Optimization - week2, Optimization algorithms
Gradient descent Batch Gradient Decent, Mini-batch gradient descent, Stochastic gradient descent 还有很 ...
- 使用Python的turtle库画圣诞树
代码如下: from turtle import * import random import time n = 80.0 speed("fastest") screensize( ...
- bzoj3262: 陌上花开(CDQ+树状数组处理三维偏序问题)
题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=3262 题目大意:中文题目 具体思路:CDQ可以处理的问题,一共有三维空间,对于第一维我们 ...
- eclipse快捷键调试总结 -转--快捷键大全
(1)Ctrl+M --切换窗口的大小(2)Ctrl+Q --跳到最后一次的编辑处(3)F2 ---重命名类名 工程名 --当鼠标放在一个标记处出现Tooltip时候按F2则把鼠标移开时To ...
- python第四天,list补充
当我们创建的列表中,元素的排列顺序常常是无法预测的,因为我们并非总能控制用户提供数据的顺序.这虽然在大多数情况下都是不可避免的,但我们经常需要以特定的顺序从呈现信息.有时候,我们希望保留列表元素最初的 ...