【进阶3-1期】JavaScript深入之史上最全--5种this绑定全面解析
this
的绑定规则总共有下面5种。
- 1、默认绑定(严格/非严格模式)
- 2、隐式绑定
- 3、显式绑定
- 4、new绑定
- 5、箭头函数绑定
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的调用位置 复制代码
使用开发者工具得到调用栈:
设置断点或者插入
debugger;
语句,运行时调试器会在那个位置暂停,同时展示当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。
2 绑定规则
2.1 默认绑定
- 独立函数调用,可以把默认绑定看作是无法应用其他规则时的默认规则,this指向全局对象。
- 严格模式下,不能将全局对象用于默认绑定,this会绑定到
undefined
。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。
function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";console.log( this.a );
}var a = 2;// 调用
foo(); // TypeError: Cannot read property 'a' of undefined// --------------------------------------function foo() { // 运行console.log( this.a );
}var a = 2;(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";foo(); // 2
})();
复制代码
2.2 隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起作用。
function foo() {console.log( this.a );
}var obj = {a: 2,foo: foo
};obj.foo(); // 2
复制代码
隐式丢失
被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
// 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {console.log( this.a );
}var obj = {a: 2,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其实引用的是foofn(); // <-- 调用位置!
}var obj = {a: 2,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: 2
};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: 2
};var bar = function() {foo.call( obj );
};bar(); // 2
setTimeout( bar, 100 ); // 2// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
复制代码
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
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
复制代码
创建一个可以重复使用的辅助函数。
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
,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
复制代码
- 2、API调用的“上下文”
JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)
一样,确保回调函数使用指定的this。这些函数实际上通过call(..)
和apply(..)
实现了显式绑定。
function foo(el) {console.log( el, this.id );
}var obj = {id: "awesome"
}var myArray = [1, 2, 3]
// 调用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(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
复制代码
手写一个new实现
function create() {// 创建一个空的对象var obj = new Object(),// 获得构造函数,arguments中去除第一个参数Con = [].shift.call(arguments);// 链接到原型,obj 可以访问到构造函数原型中的属性obj.__proto__ = Con.prototype;// 绑定 this 实现继承,obj 可以访问到构造函数中的属性var ret = Con.apply(obj, arguments);// 优先返回构造函数返回的对象return ret instanceof Object ? ret : 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, [2, 3] ); // a:2,b:3// 使用bind(..)进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // 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( ø, [2, 3] ); // a:2,b:3// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3
复制代码
4.2 间接引用
间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
// p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo()
function foo() {console.log( this.a );
}var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};o.foo(); // 3
(p.foo = o.foo)(); // 2
复制代码
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, 1 ); 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;};
}
复制代码
使用:软绑定版本的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, 10 ); // 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: 2
};var obj2 = {a: 3
}var bar = foo.call( obj1 );
bar.call( obj2 ); // 2,不是3!
复制代码
ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。
function foo() {var self = this; // lexical capture of thissetTimeout( function() {console.log( self.a ); // self只是继承了foo()函数的this绑定}, 100 );
}var obj = {a: 2
};foo.call(obj); // 2
复制代码
代码风格统一问题:如果既有this风格的代码,还会使用 seft = this
或者箭头函数来否定this机制。
- 只使用词法作用域并完全抛弃错误this风格的代码;
- 完全采用this风格,在必要时使用
bind(..)
,尽量避免使用self = this
和箭头函数。
上期思考题解
代码1:
var scope = "global scope";
function checkscope(){var scope = "local scope";function f(){return scope;}return f;
}checkscope()();
复制代码
代码2:
var scope = "global scope";
function checkscope(){var scope = "local scope";function f(){return scope;}return f;
}var foo = checkscope();
foo();
复制代码
上面的两个代码中,checkscope()
执行完成后,闭包f
所引用的自由变量scope
会被垃圾回收吗?为什么?
解答:
checkscope()
执行完成后,代码1中自由变量特定时间之后回收,代码2中自由变量不回收。
首先要说明的是,现在主流浏览器的垃圾回收算法是标记清除,标记清除并非是标记执行栈的进出,而是从根开始遍历,也是一个找引用关系的过程,但是因为从根开始,相互引用的情况不会被计入。所以当垃圾回收开始时,从Root(全局对象)开始寻找这个对象的引用是否可达,如果引用链断裂,那么这个对象就会回收。
闭包中的作用域链中 parentContext.vo 是对象,被放在堆中,栈中的变量会随着执行环境进出而销毁,堆中需要垃圾回收,闭包内的自由变量会被分配到堆上,所以当外部方法执行完毕后,对其的引用并没有丢。
每次进入函数执行时,会重新创建可执行环境和活动对象,但函数的[[Scope]]
是函数定义时就已经定义好的(词法作用域规则),不可更改。
- 对于代码1:
checkscope()
执行时,将checkscope
对象指针压入栈中,其执行环境变量如下
checkscopeContext:{AO:{arguments:scope:f:},this,[[Scope]]:[AO, globalContext.VO]
}
复制代码
执行完毕后出栈,该对象没有绑定给谁,从Root开始查找无法可达,此活动对象一段时间后会被回收
- 对于代码2:
checkscope()
执行后,返回的是f
对象,其执行环境变量如下
fContext:{AO:{arguments:},this,[[Scope]]:[AO, checkscopeContext.AO, globalContext.VO]
}
复制代码
此对象赋值给var foo = checkscope();
,将foo
压入栈中,foo
指向堆中的f
活动对象,对于Root
来说可达,不会被回收。
如果一定要自由变量scope
回收,那么该怎么办???
很简单,foo = null;
,把引用断开就可以了。
本期思考题
依次给出console.log输出的数值。
var num = 1;
var myObject = {num: 2,add: function() {this.num = 3;(function() {console.log(this.num);this.num = 4;})();console.log(this.num);},sub: function() {console.log(this.num)}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
复制代码
参考
你不知道的JavaScript上卷—笔记
Javascript 闭包,引用的变量是否被回收?
进阶系列目录
- 【进阶1期】 调用堆栈
- 【进阶2期】 作用域闭包
- 【进阶3期】 this全面解析
- 【进阶4期】 深浅拷贝原理
- 【进阶5期】 原型Prototype
- 【进阶6期】 高阶函数
- 【进阶7期】 事件机制
- 【进阶8期】 Event Loop原理
- 【进阶9期】 Promise原理
- 【进阶10期】Async/Await原理
- 【进阶11期】防抖/节流原理
- 【进阶12期】模块化详解
- 【进阶13期】ES6重难点
- 【进阶14期】计算机网络概述
- 【进阶15期】浏览器渲染原理
- 【进阶16期】webpack配置
- 【进阶17期】webpack原理
- 【进阶18期】前端监控
- 【进阶19期】跨域和安全
- 【进阶20期】性能优化
- 【进阶21期】VirtualDom原理
- 【进阶22期】Diff算法
- 【进阶23期】MVVM双向绑定
- 【进阶24期】Vuex原理
- 【进阶25期】Redux原理
- 【进阶26期】路由原理
- 【进阶27期】VueRouter源码解析
- 【进阶28期】ReactRouter源码解析
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
github.com/yygmind/blo…
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
【进阶3-1期】JavaScript深入之史上最全--5种this绑定全面解析相关推荐
- this指向undefined uiapp_JavaScript深入之史上最全5种this绑定全面解析
this的绑定规则总共有下面5种. 1.默认绑定(严格/非严格模式) 2.隐式绑定 3.显式绑定 4.new绑定 5.箭头函数绑定 1 调用位置 调用位置就是函数在代码中被调用的位置(而不是声明的位置 ...
- 计算机集成电板 ppt,史上最全,PCB板和集成电路解析(干货分享)
原标题:史上最全,PCB板和集成电路解析(干货分享) 目前的电路板,主要由以下组成: 线路与图面(Pattern):线路是做为原件之间导通的工具,在设计上会另外设计大铜面作为接地及电源层.线路与图面是 ...
- java2019 数据结构算法面试题_GitHub - sjyw/java-interview: 史上最全Java面试题汇总与解析(505道):2019最新版...
Java 面试全解析 知识树 课程亮点 500道 Java 常见面试题 + 14万字核心知识解析 丰富的 Java 技术栈:基础和框架,线程池和锁优化,SpringBoot 和分布式消息队列,数据结构 ...
- 微信小程序—史上最全78种炫到爆的自定义动画
摘要 最近在研究小程序,搜集整理而来共计78种自定义动画,现分享给大家,并且写成demo,读者可通过体验获取想要的效果,复制粘贴使用,希望能共同学习. 部分效果图 体验途径 依次点击:custom系列 ...
- 史上最全| 14种自动化分拣系统合集
导语 大家好,我是智能仓储物流技术研习社的社长,你的老朋友,老K. 新书上市<智能物流系统构成与技术实践> 2023年度-厂商宣传合作位--->点击详情 本文为研习社原创,违规转载必 ...
- 史上最全| 14种自动化分拣系统合集|拿走!
导语 大家好,我是智能仓储物流技术研习社的社长,老K.今天给大家整理了14种不同种类的自动化分拣方式,供大家参考. 01移栽式 02偏转轮式 03扫臂式 04滑靴式 05侧向翻转 06推盘式 07交叉 ...
- 史上最全大数据实践应用案例解析
目前最热门,最有发展前景的当属金融和IT行业了,而这两个行业的蓬勃发展自然离不开大数据.在金融领域中银行和保险业尤其适用于大数据技术,现在汇丰银行,英杰华等众多公司已通过分析海量数据大受裨益.举个例子 ...
- 史上最全的vue.js源码解析(四)
虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下.vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到.所以我近期会对vue.js的源码进行解读,分享值得去学习 ...
- 呕心沥血集齐史上最全 JavaScript最实用工具函数大全,建议收藏!
为元素添加on方法 Element.prototype.on = Element.prototype.addEventListener; NodeList.prototype.on = functio ...
最新文章
- 中国闪存联盟三百大行动成果发布 IBM存储助力企业突破数据临界点
- property field java_Java 中 field 和 variable 区别及相关术语解释
- 通用技术和信息技术合格考知识点_高二信息与通用技术会考知识点
- uva 10771——Barbarian tribes
- java 使用logback进行日志输出
- 【Spark】SparkStreaming之windows操作
- Jvm(29),理解升级----C语言中的堆和栈的区别 (可以借鉴)
- Codeforces 385 C Bear and Prime Numbers
- java二维数组扫雷,Java 数组 之 二维数组 扫雷实例
- VUE项目开发的完整流程
- 百度地图API之根据经纬度查询地址信息(Android)
- 广东工业大学计算机学院张静,广东工业大学文件.doc
- pytorch使用DCN
- C#反编译之:<PrivateImplementationDetails>.ComputeStringHash 错误
- 基于软路由连接上网的相关配置
- springboot项目jar包发布的,如何线上修改jar包。
- 借助阿里云轻松部署企业网盘
- 关于cv::warpaffine函数
- 江湖问题研究-- intent传递有没有大小限制,是多少?
- 在Unity里将多个Sprite(精灵图)动态合成一个Sprite