js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解
壹 ❀ 引
可以说this与闭包、原型链一样,属于JavaScript开发中老生常谈的问题了,百度一搜,this相关的文章铺天盖地。可开发好几年,被几道this题安排明明白白的人应该不在少数(我就是其一)。我觉得this概念抽象,变化多端总是让人晕头转向,但平心它并不是有多难,今天我们就从this绑定的五种场景(默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定)出发,静下心来好好聊聊这个 this,本文开始。
贰 ❀ this默认绑定
this默认绑定我们可以理解为函数调用时无任何调用前缀的情景,它无法应对我们后面要介绍的另外四种情况,所以称之为默认绑定,默认绑定时this指向全局对象(非严格模式):
function fn1() {
let fn2 = function () {
console.log(this); //window
fn3();
};
console.log(this); //window
fn2();
}; function fn3() {
console.log(this); //window
}; fn1();
这个例子中无论函数声明在哪,在哪调用,由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window。
但需要注意的是,在严格模式环境中,默认绑定的this指向undefined,来看个对比例子:
function fn() {
console.log(this); //window
console.log(this.name);
}; function fn1() {
"use strict";
console.log(this); //undefined
console.log(this.name);
}; var name = '听风是风'; fn();
fn1() //TypeError: Cannot read property 'a' of undefined
再例如函数以及调用都暴露在严格模式中的例子:
"use strict";
var name = '听风是风';
function fn() {
console.log(this); //undefined
console.log(this.name);//报错
};
fn();
最后一点,如果在严格模式下调用不在严格模式中的函数,并不会影响this指向,来看最后一个例子:
var name = '听风是风';
function fn() {
console.log(this); //window
console.log(this.name); //听风是风
}; (function () {
"use strict";
fn();
}());
叁 ❀ this隐式绑定
1.隐式绑定
什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,看个例子:
function fn() {
console.log(this.name);
};
let obj = {
name: '听风是风',
func: fn
};
obj.func() //听风是风
如果函数调用前存在多个对象,this指向距离调用自己最近的对象,比如这样:
function fn() {
console.log(this.name);
};
let obj = {
name: '行星飞行',
func: fn,
};
let obj1 = {
name: '听风是风',
o: obj
};
obj1.o.func() //行星飞行
那如果我们将obj对象的name属性注释掉,现在输出什么呢?
function fn() {
console.log(this.name);
};
let obj = {
func: fn,
};
let obj1 = {
name: '听风是风',
o: obj
};
obj1.o.func() //??
这里输出undefined,大家千万不要将作用域链和原型链弄混淆了,obj对象虽然obj1的属性,但它两原型链并不相同,并不是父子关系,由于obj未提供name属性,所以是undefined。
既然说到原型链,那我们再来点花哨的,我们再改写例子,看看下面输出多少:
function Fn() {};
Fn.prototype.name = '时间跳跃'; function fn() {
console.log(this.name);
}; let obj = new Fn();
obj.func = fn; let obj1 = {
name: '听风是风',
o: obj
};
obj1.o.func() //?
这里输出时间跳跃,虽然obj对象并没有name属性,但顺着原型链,找到了产生自己的构造函数Fn,由于Fn原型链存在name属性,所以输出时间跳跃了。
番外------作用域链与原型链的区别:
当访问一个变量时,解释器会先在当前作用域查找标识符,如果没有找到就去父作用域找,作用域链顶端是全局对象window,如果window都没有这个变量则报错。
当在对象上访问某属性时,首选i会查找当前对象,如果没有就顺着原型链往上找,原型链顶端是null,如果全程都没找到则返一个undefined,而不是报错。
2.隐式丢失
在特定情况下会存在隐式绑定丢失的问题,最常见的就是作为参数传递以及变量赋值,先看参数传递:
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
}; function fn1(param) {
param();
};
fn1(obj.fn);//行星飞行
这个例子中我们将 obj.fn 也就是一个函数传递进 fn1 中执行,这里只是单纯传递了一个函数而已,this并没有跟函数绑在一起,所以this丢失这里指向了window。
第二个引起丢失的问题是变量赋值,其实本质上与传参相同,看这个例子:
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
};
let fn1 = obj.fn;
fn1(); //行星飞行
注意,隐式绑定丢失并不是都会指向全局对象,比如下面的例子:
var name = '行星飞行';
let obj = {
name: '听风是风',
fn: function () {
console.log(this.name);
}
};
let obj1 = {
name: '时间跳跃'
}
obj1.fn = obj.fn;
obj1.fn(); //时间跳跃
虽然丢失了 obj 的隐式绑定,但是在赋值的过程中,又建立了新的隐式绑定,这里this就指向了对象 obj1。
肆 ❀ this显式绑定
显式绑定是指我们通过call、apply以及bind方法改变this的行为,相比隐式绑定,我们能清楚的感知 this 指向变化过程。来看个例子:
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
let obj3 = {
name: 'echo'
}
var name = '行星飞行'; function fn() {
console.log(this.name);
};
fn(); //行星飞行
fn.call(obj1); //听风是风
fn.apply(obj2); //时间跳跃
fn.bind(obj3)(); //echo
比如在上述代码中,我们分别通过call、apply、bind改变了函数fn的this指向。
在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而call与apply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。
注意,如果在使用call之类的方法改变this指向时,指向参数提供的是null或者undefined,那么 this 将指向全局对象。
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
var name = '行星飞行'; function fn() {
console.log(this.name);
};
fn.call(undefined); //行星飞行
fn.apply(null); //行星飞行
fn.bind(undefined)(); //行星飞行
另外,在js API中部分方法也内置了显式绑定,以forEach为例:
let obj = {
name: '听风是风'
}; [1, 2, 3].forEach(function () {
console.log(this.name);//听风是风*3
}, obj);
番外-----call、apply与bind有什么区别?
1.call、apply与bind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为什么上方例子中bind后还加了一对括号 ()的原因。
2.bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
3.call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组。
描述一请参照上面已有例子。
描述二请参照下方例子,我们尝试修改 boundFunction 的 this 指向:
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
var name = '行星飞行'; function fn() {
console.log(this.name);
};
fn.call(obj1); //听风是风
fn(); //行星飞行
fn.apply(obj2); //时间跳跃
fn(); //行星飞行
let boundFn = fn.bind(obj1);//听风是风
boundFn.call(obj2);//听风是风
boundFn.apply(obj2);//听风是风
boundFn.bind(obj2)();//听风是风
描述三请参考以下例子:
let obj = {
name: '听风是风'
}; function fn(age,describe) {
console.log(`我是${this.name},我的年龄是${age},我非常${describe}!`);
};
fn.call(obj,'26','帅');//我是听风是风,我的年龄是26,我非常帅
fn.apply(obj,['26','帅']);//我是听风是风,我的年龄是26,我非常帅
更多关于call apply bind可以阅读博主这篇文章 js中call、apply、bind到底有什么区别?bind返回的方法还能修改this指向吗?
伍 ❀ new绑定
准确来说,js中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。
那么new一个函数究竟发生了什么呢,大致分为三步:
1.以构造器的prototype属性为原型,创建新对象;
2.将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行;
3.如果构造器没有手动返回对象,则返回第一步创建的对象
这个过程我们称之为构造调用,我们来看个例子:
function Fn(){
this.name = '听风是风';
};
let echo = new Fn();
echo.name//听风是风
在上方代码中,构造调用创建了一个新对象echo,而在函数体内,this将指向新对象echo上(可以抽象理解为新对象就是this)。
若对于new具体过程有疑惑,或者不知道怎么手动实现一个new 方法,可以阅读博主这篇文章 js new一个对象的过程,实现一个简单的new方法
陆 ❀ this绑定优先级
我们先介绍前四种this绑定规则,那么问题来了,如果一个函数调用存在多种绑定方法,this最终指向谁呢?这里我们直接先上答案,this绑定优先级为:
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定
为什么显式绑定不和new绑定比较呢?因为不存在这种绑定同时生效的情景,如果同时写这两种代码会直接抛错,所以大家只用记住上面的规律即可。
function Fn(){
this.name = '听风是风';
};
let obj = {
name:'行星飞行'
}
let echo = new Fn().call(obj);//报错 call is not a function
那么我们结合几个例子来验证下上面的规律,首先是显式大于隐式:
//显式>隐式
let obj = {
name:'行星飞行',
fn:function () {
console.log(this.name);
}
};
obj1 = {
name:'时间跳跃'
};
obj.fn.call(obj1);// 时间跳跃
其次是new绑定大于隐式:
//new>隐式
obj = {
name: '时间跳跃',
fn: function () {
this.name = '听风是风';
}
};
let echo = new obj.fn();
echo.name;//听风是风
柒 ❀ 箭头函数的this
ES6的箭头函数是另类的存在,为什么要单独说呢,这是因为箭头函数中的this不适用上面介绍的四种绑定规则。
准确来说,箭头函数中没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁。有点吃软饭的嫌疑,一点都不硬朗,我们来看个例子:
function fn() {
return () => {
console.log(this.name);
};
}
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
let bar = fn.call(obj1); // fn this指向obj1
bar.call(obj2); //听风是风
为啥我们第一次绑定this并返回箭头函数后,再次改变this指向没生效呢?
前面说了,箭头函数的this取决于外层作用域的this,fn函数执行时this指向了obj1,所以箭头函数的this也指向obj1。除此之外,箭头函数this还有一个特性,那就是一旦箭头函数的this绑定成功,也无法被再次修改,有点硬绑定的意思。
当然,箭头函数的this也不是真的无法修改,我们知道箭头函数的this就像作用域继承一样从上层作用域找,因此我们可以修改外层函数this指向达到间接修改箭头函数this的目的。
function fn() {
return () => {
console.log(this.name);
};
};
let obj1 = {
name: '听风是风'
};
let obj2 = {
name: '时间跳跃'
};
fn.call(obj1)(); // fn this指向obj1,箭头函数this也指向obj1
fn.call(obj2)(); //fn this 指向obj2,箭头函数this也指向obj2
捌 ❀ 总
那么到这里,对于this的五种绑定场景就全部介绍完毕了,如果你有结合例子练习下来,我相信你现在对于this的理解一定更上一层楼了。
那么通过本文,我们知道默认绑定在严格模式与非严格模式下this指向会有所不同。
我们知道了隐式绑定与隐式丢失的几种情况,并简单复习了作用域链与原型链的区别。
相对隐式绑定改变的不可见,我们还介绍了显式绑定以及硬绑定,简单科普了call、apply与bind的区别,并提到当绑定指向为null或undefined时this会指向全局(非严格模式)。
我们介绍了new绑定以及new一个函数会发生什么。
最后我们了解了不太合群的箭头函数中的this绑定,了解到箭头函数的this由外层函数this指向决定,并有一旦绑定成功也无法再修改的特性。
希望在面试题中遇到this的你不再有所畏惧,到这里,本文结束。
对了,学完了this不妨来两道面试题试试自己的理解情况,带详细解析:js 从两道面试题加深理解闭包与箭头函数中的this
参考
你不知道的js中关于this绑定机制的解析[看完还不懂算我输]
JavaScript深入之史上最全--5种this绑定全面解析
js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解的更多相关文章
- JS五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解(转载)
目录 壹 ❀ 引 贰 ❀ this默认绑定 叁 ❀ this隐式绑定 1.隐式绑定 2.隐式丢失 肆 ❀ this显式绑定 伍 ❀ new绑定 陆 ❀ this绑定优先级 柒 ❀ 箭头函数的this ...
- [转]js模块化编程之彻底弄懂CommonJS和AMD/CMD!
原文: https://www.cnblogs.com/chenguangliang/p/5856701.html ------------------------------------------ ...
- js五种设计模式说明与示例
第一种模式:js工厂模式 var lev=function(){ return "啊打"; }; function Parent(){ ...
- js五种基本数据类型:string, number, boolean, null, undefined
/** * 五种基本数据类型:string, number, boolean, null, undefined */ // undefined // 声明变量foo,未声明变量bar var foo; ...
- Windows提供了两种将DLL映像到进程地址空间的方法(隐式和显式)
调用DLL,首先需要将DLL文件映像到用户进程的地址空间中,然后才能进行函数调用,这个函数和进程内部一般函数的调用方法相同.Windows提供了两种将DLL映像到进程地址空间的方法: 1. 隐式的加载 ...
- js模块化编程之彻底弄懂CommonJS和AMD/CMD!
先回答我:为什么模块很重要? 答:因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块.但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写 ...
- Python——五分钟带你弄懂迭代器与生成器,夯实代码能力
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是周一Python专题,给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉 ...
- js五种设计模式
1.js工厂模式 var lev=function(){ return "嘿哈"; }; function Parent(){ var Child = new object(); ...
- 面试题:实现一个方法clone;可以对js五种数据类型进行值复制
//先来方法的代码function clone(obj) { var copy; switch(typeof obj){ case 'number': case 'string': case 'boo ...
随机推荐
- 【Android - 进阶】之Animation补间动画
补间动画也叫View动画,它只能针对View进行动画操作,且补间动画操作的只是View中可见的部分,即只操作界面,对于可点击区域等都不会进行操作. 在Android中,补间动画的顶级类是Animati ...
- xpath-房价爬取
xpath xpath两种使用方式 和bs类似,一种是调用本地资源,一种是网络资源 etree.parse(filePath) etree.HTML('page_text') xpath表达式 层级: ...
- 如何解决jpa 要求column 名称单词必须用下划线
[转]:http://www.jeesns.cn/article/detail/6657 先引出轮子http://blog.csdn.net/54powerman/article/details/76 ...
- (一)sync分析之为啥el-dialog中的visible需要使用.sync
首先,笔者在使用element-ui 中的dialog组件时,发现visible属性在使用时需要添加.sync才生效,心中好奇,所以研究一下原理 我们先自己创建一个dialog组件,如下 当我们点击关 ...
- 目标检测 1 : 目标检测中的Anchor详解
咸鱼了半年,年底了,把这半年做的关于目标的检测的内容总结下. 本文主要有两部分: 目标检测中的边框表示 Anchor相关的问题,R-CNN,SSD,YOLO 中的anchor 目标检测中的边框表示 目 ...
- 大规模机器学习在LinkedIn预测模型中的应用实践
预测模型在 LinkedIn 的产品中被广泛应用,如 Feed.广告.工作推荐.邮件营销.用户搜索等.这些模型在提升用户体验时起到了重要的作用.为了满足建模需求,LinkedIn 开发并且开源了 Ph ...
- Maven学习整理
1. window系统中安装Maven 2. 解决使用代理联网时无法下载jar包 3. 配置本地仓库 4. 处理本地仓库和中央仓库都没有依赖的jar包的情况
- 数据库Oracle日期函数
SYSDATE 函数:是一个日期函数,它返回当前数据库服务器的日期和时间. 用日期计算: • 从日期加或者减一个数,结果是一个日期值 • 两个日期相减,得到两个日期之间的天数 ,可以加小时到日期上 S ...
- Python字符串学习
Python字符串(不可变的): 一.相关的运算: 1.字符串的拼接: str = str1 + str2 2.字符串的重复: print(str * 3) 3.下标访问字符串某个字符: str[1] ...
- 基于CC1606 FPGA评估板移植iCamera程序小结
iCamera作为柴草电子经典的摄像头开发工具,其强大的摄像头调试功能,深受广大网友喜爱,支持市面上各种摄像头. 目前现有的应用板卡支持:CC1601(CP601A). CC1602(CP601B) ...