壹 ❀ 引

可以说this与闭包、原型链一样,属于JavaScript开发中老生常谈的问题了,但开发好几年,依然被几道this指向题安排明明白白的人应该不在少数。对于初学者而言,this概念抽象,变化多端总是让人晕头转向,但平心而论它并没有多难,今天我们就从this绑定的五种场景(默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定)出发,静下心来好好聊聊这个this,本文开始。

贰 ❀ this默认绑定

this默认绑定我们可以理解为函数调用时无任何调用前缀的情景,它无法应对我们后面要介绍的另外四种情况,所以称之为默认绑定,默认绑定时this指向全局对象(非严格模式):

function fn1() {let fn2 = function () {console.log(this); //windowfn3();};console.log(this); //windowfn2();
};function fn3() {console.log(this); //window
};fn1();

这个例子中无论函数声明在哪,在哪调用,由于函数调用时前面并未指定任何对象,这种情况下this指向全局对象window

但需要注意的是,在严格模式环境中,默认绑定的this指向undefined,来看个对比例子:

function fn() {console.log(this); //windowconsole.log(this.name);
};function fn1() {"use strict";console.log(this); //undefinedconsole.log(this.name);
};var name = '听风是风';fn();
fn1() //TypeError: Cannot read property 'name' of undefined

再例如函数以及调用都暴露在严格模式中的例子:

"use strict";
var name = '听风是风';
function fn() {console.log(this); //undefinedconsole.log(this.name);//报错
};
fn();

最后一点,如果在严格模式下调用不在严格模式中的函数,并不会影响this指向,来看最后一个例子:

var name = '听风是风';
function fn() {console.log(this); //windowconsole.log(this.name); //听风是风
};(function () {"use strict";fn();
}());

叁 ❀ this隐式绑定

叁 ❀ 壹 隐式绑定

什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么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属性,所以输出时间跳跃了。

叁 ❀ 贰 隐式丢失

在特定情况下会存在隐式绑定丢失的问题,最常见的就是作为参数传递以及变量赋值,先看参数传递:

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改变了函数fnthis指向。

在js中,当我们调用一个函数时,我们习惯称之为函数调用,函数处于一个被动的状态;而callapply让函数从被动变主动,函数能主动选择自己的上下文,所以这种写法我们又称之为函数应用。

注意,如果在使用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有什么区别?

  • call、applybind都用于改变this绑定,但call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数,这也是为什么上方例子中bind后还加了一对括号 ()的原因。
  • bind属于硬绑定,返回的boundFunctionthis 指向无法再次通过bind、applycall 修改;callapply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
  • callapply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组。

描述一请参照上面已有例子。

描述二请参照下方例子,我们尝试修改 boundFunctionthis指向:

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,我非常帅

伍 ❀ 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的区别,并提到当绑定指向为nullundefinedthis会指向全局(非严格模式)。

我们介绍了new绑定以及new一个函数会发生什么。

最后我们了解了不太合群的箭头函数中的this绑定,了解到箭头函数的this由外层函数this指向决定,并有一旦绑定成功也无法再修改的特性。

希望在面试题中遇到this的你不再有所畏惧,到这里,本文结束。

对了,学完了this不妨来两道面试题试试自己的理解情况,带详细解析:[js 从两道面试题加深理解闭包与箭头函数中的this](

五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解相关推荐

  1. js 中的this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

    壹 ❀ 引 工具猴-免费在线工具-在线工具箱- 可以说this与闭包.原型链一样,属于JavaScript开发中老生常谈的问题了,百度一搜,this相关的文章铺天盖地.可开发好几年,被几道this题安 ...

  2. python getopt_python 5种 statsPython中的getopt函数使用详解

    函数原型: getopt.getopt(args, shortopts, longopts=[]) 参数解释: args:args为需要解析的参数列表.一般使用sys.argv[1:],这样可以过滤掉 ...

  3. 5-Selenium WebDriver三种等待--隐式等待-显式等待和流畅等待

    在selenium中,Waits在执行测试中扮演重要角色.在本教程中,您将学习Selenium中"隐式"和"显式"等待的各个方面. 在Selenium中为什么需 ...

  4. hwt字体转换ttf_五分钟教你弄懂了字体反爬是个啥

    今天的文章内容主要是关于字体反爬. 目前已知的几个字体反爬的网站是猫眼,汽车之家,天眼查,起点中文网等等. 以前也看过这方面的文章,今天跟个老哥在交流的时候,终于实操了一把,弄懂了字体反爬是个啥玩意. ...

  5. sklearn与机器学习系列专题之降维(五)一文弄懂Isomap特征筛选降维

    目录 1.Isomap算法简介 2.Isomap算法原理 3.Isomap算法优缺点 4.python实战Isomap 5.下篇预告 1.Isomap算法简介 等度量映射(Isometric Feat ...

  6. 只需五步学会Maven 3.6.1OR 3.6.3及其他版本的下载安装与配置【图文详解】

    第一步,下载并解压缩包 ​第二步,配置两个环境变量 ​第三步,测试是否安装成功 ​第四步,指定本地仓库的路径 第五步,修改镜像仓库 第一步,下载并解压缩包 Maven官方下载地址:https://ma ...

  7. 网页访问服务器默认什么端口号,URL中没有端口号默认为80?详解IT产品必须熟知的访问地址概念!...

    原标题:URL中没有端口号默认为80?详解IT产品必须熟知的访问地址概念! 作为一个互联网产品经理,你可能每天打开几十个网页,但你有没有观察过他们的网址? 作者带你庖丁解牛并把网址拆开.标准URL地址 ...

  8. 【两种解法】Quadtrees UVA - 297(隐式建树+显式建树)

    立志用最少的代码做最高效的表达 A quadtree is a representation format used to encode images. The fundamental idea be ...

  9. dev c++如何恢复默认设置_C编程从入门到实践:C语言开发工具详解(2)

    DEV C++是一款经典的轻量级C语言开发工具,其安装大小只有几十兆,并且具有图形视图界面,操作比较容易.在DEV C++编码界面中可以使用复制和粘贴等命令,这提高了开发效率. 2.3.1安装DEV ...

最新文章

  1. rust 官服指令_RUST 命令大全(包括服务器指令)
  2. 怎样实现企业管理系统的操作日志功能
  3. DataGridView 的 CurrentCellDirtyStateChanged事件用法
  4. 打包指令_DD19A电动热熔打包机,更换易损件方法
  5. 拿了年终奖后,发现自己又拖后腿了?对不起,可能事实并没有那么糟糕...
  6. 微型计算机作为载体的部件是,大工11秋《计算机应用基础》辅导资料二
  7. python 打开本地程序发生异常_Python中的异常处理
  8. php socket访问单片机,可运行在单片机上的UDP通讯协议的实现【上】
  9. 今天起改用mac的marsedit写博
  10. 明小子动力上传拿webshell.zip
  11. Java中的正则表达式
  12. element-ui下载到本地方法(python 或java)
  13. linux利用源码安装madplay
  14. 【实用工具系列之爬虫】python实现爬取代理IP(防 ‘反爬虫’)
  15. Anaconda3 从navigator面板无法启动
  16. Eclipse+Maven配置TestNG框架
  17. 计算机网络用户名及密码如何查询,用wifi连接电脑的怎么查看宽带账号密码
  18. 千里之行,始于驭风——咕咚新款21k驭风跑鞋体验
  19. 大数据时代,python竟是最好的语言?
  20. 影响搜索引擎排名的因素有哪些

热门文章

  1. 《Python核心编程(第3版)》学习笔记及书评
  2. 传新浪未获微博运营牌照 新浪官方对此辟谣
  3. App store上传提交详细流程
  4. 乌班图服务器系统网卡驱动,开源之系统:Ubuntu20.04电脑安装无线网卡驱动并解决包依赖关系...
  5. 数睿数据新签约6家合作伙伴,第六届山东软件生态大会顺利落幕
  6. myBatis MybatisMapperMethod类 获取 Page对象的坑。。
  7. Windows之CMD命令合集
  8. 浅谈博弈论与经济学的关系
  9. 3维向量的点乘叉乘运算
  10. SQL删除多列语句的写法