前端进击的巨人(六):知否知否,须知this
常见this的误解
- 指向函数自身(源于this英文意思的误解)
- 指向函数的词法作用域(部分情况)
this的应用环境
1. 全局环境
无论是否在严格模式下,全局执行环境中(任何函数体外部)this
都指向全局对象
var name = '以乐之名';
this.name; // 以乐之名
2. 函数(运行内)环境
函数内部,this
的值取决于函数被调用的方式(被谁调用)
var name = '无名氏';
function getName() {
console.log(this.name);
}
getName(); // 无名氏 调用者是全局对象
var myInfo = {
name: '以乐之名',
getName: getName
};
myInfo.getName(); // 以乐之名 调用者是myInfo对象
this的正解
"this的指向是在运行时进行绑定的,而不是代码书写(函数声明)时确定!!!"
"看谁用",this的指向取决于调用者,这也是很多文章提到过的观点。"谁调用,this指向谁",只是这句话稍有偏颇,某些情况不见得都适用。
生活栗子:你的钱并不一定是你的钱,只有当你使用消费了才是你的钱 。
("看谁用"),借出去的钱就不是你的了。。。
回到正文,我们先通过栈,来理解什么是调用位置?
JavaScript中函数的调用是以栈的方式来存储,栈顶是正在运行的函数,函数调用时入栈,执行完成后出栈。
function foo() {
// 此时的栈:全局 -> foo,调用位置在foo
bar();
}
function bar() {
// 此时的栈:全局 -> foo -> bar,调用位置在bar
baz();
}
function baz() {
// 此时的栈:全局 -> foo -> bar -> baz,调用位置在baz
// ...
}
foo();
代码中虽然函数存在多层嵌套使用,但处于栈顶的只有正在执行的函数,也即调用者只有顶层的那一个(或最后一个),理清调用位置(调用者)有助于我们理解this
。
this的绑定规则
- 默认绑定(函数单独调用)
- 隐式绑定(作为对象的属性方法调用,带有执行上下文)
- 显示绑定(
call/apply/bind
) new
绑定(new
创建实例)- 箭头函数绑定(ES6新增,基于词法作用域)
默认绑定下(函数单独调用)区分严格模式
- 非严格模式,
this
会指向全局对象(浏览器全局对象是window
,NodeJS全局对象是global
); - 严格模式,
this
指向undefined
// 非严格模式
function getName() {
console.log(this.name); // this指向全局对象
}
getName(); // "",并不会报错,如果外部有全局变量name,则会输出对应值
// 严格模式
function getName() {
"use strict"
console.log(this.name); // this指向undefined
}
getName(); // TypeError: Cannot read property 'name' of undefined
TIPS: 严格模式中,对函数中this的影响,只在函数内声明了严格模式才会存在,如果是调用时声明严格模式则不会影响。
function getName() {
console.log(this.name);
}
// 调用时声明严格模式
"use strict";
getName(); // ""
隐式绑定
隐式绑定中,函数一般作为对象的属性调用,带有调用者的执行上下文。因此this
值取决于调用者的上下文环境。如果存在多层级属性引用,只有对象属性引用链中最顶层(最后一层)会影响调用位置,而this
的值取决于调用位置。文章开头以栈来理解调用者的例子。
function getName() {
return this.name;
}
var myInfo = {
name: '以乐之名',
getName: getName
};
var leader = {
name: '大神组长'
man: myInfo
};
leader.man.getName(); // '以乐之名'
// man 指向 myInfo,最顶层(最后一层)对象为 myInfo
apply/call的区别
apply/call
方法两者类似,都可以显示绑定this
,两者的区别是参数传递的方式不同。apply/call
第一个参数都为要指定this
的对象,不同的是apply
第二个参数接受的是一个参数数组,而call
从第二个参数开始接受的是参数列表。
apply语法:func.apply(thisArg, [argsArray])
call语法:func.call(thisArg, arg1, arg2, ...)
var numbers = [5, 6, 2, 3, 7];
// 求numbers的最大值
// apply
var max = Math.max.apply(null, numbers);
// call
var max = Math.max.call(null, ...numbers); // ...展开运算符
TIPS: 如果thisArg为原始值(数字,字符串,布尔值),this
会指向该原始值的自动包装对象,如Number
, String
, Boolean
等
func.apply(1);
// func中的this -> Number对象;
bind的特别(柯里化的应用)
bind
是ES5新增的方法,跟apply/call
功能一样,可以显示绑定this。
bind语法:function.bind(thisArg[, arg1[, arg2[, ...]]])
bind()方法创建一个新的函数,在调用时设置this关键字为提供的值,并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
-- 《Function.prototype.bind() | MDN》
"bind与apply/call的区别:apply/call传入this并立即执行函数,而bind传入this则返回一个函数,并不会立即执行,只有调用返回的函数才会执行原始函数"。
bind
方法是函数柯里化的一种应用,看过上篇《前端进击的巨人(五):学会函数柯里化(curry) 》的小伙伴,应该还记得"函数柯里化的特点:延迟执行,部分传参,返回一个可处理剩余参数的函数"。
bind
相较apply/call
的优点,可以通过部分传参提前对this进行一次"永久绑定",也就是说this
只需绑定一次,省却每次执行都要进行this
绑定的操作。
function getName() {
return this.name;
}
var myInfo = {
name: '以乐之名',
job: '前端工程师'
};
var getName = getName.bind(myInfo);
getName(); // '以乐之名';
getName(); // '以乐之名';
// 一次性绑定,之后调用无需再修改this
TIPS: 函数柯里化可以用于参数预设,像一次性操作(判断/绑定)等。
有关函数柯里化的详解,请回阅:《前端进击的巨人(五):学会函数柯里化(curry) 》。
构造函数中的this
通过new
操作符可以实现对函数的构造调用。JavaScript中本身并没有"构造函数",一个函数如果没有使用new
操作符调用,那么它就是个普通函数,new Func()
实际上是对函数Func
的"构造调用"。
在了解构造函数中的this
前,有必要先了解下new
实例化对象的过程。
new实例过程
- 创建(构造)一个全新的空对象
- 这个新对象会被执行"原型"链接(新对象的
__proto__
会指向函数的prototype
) - 构造函数的
this
会指向这个新对象,并对this
属性进行赋值 - 如果函数没有返回其他对象,则返回这个新对象(注意构造函数的
return
,一般不会有return
)
// 正常不带return的构造函数
function People(name, sex) {
this.name = name;
this.sex = sex;
}
var man = new People('亚当', '男');
var woman = new People('夏娃', '女');
// 实例化对象成功
// 构造函数带了return
function People(name, sex) {
return 1; // 返回的是Number对象
}
function People(name, sex) {
return 'hello world'; // 返回的是String对象
}
function People(name, sex) {
return function() {}
}
function People(name, sex) {
return {};
}
// 以上并未正确实例化对象
构造函数自定义return
,会造成new
无法完成正确的实例化操作。如果返回值为基本类型,则返回其包装对象Number/String/Bollean
。
TIPS: 原型链中的this指向其实例化的对象
People.prototype.say = function() {
console.log(`我的名字:${this.name}`);
};
var man = new People('亚当', '男');
man.say(); // 我的名字:亚当
this绑定规则的优先级
显示绑定 / new
绑定 > 隐式绑定 > 默认绑定
TIPS: new
无法跟apply/call
同时使用
this判定步骤
- 函数被
new
操作符使用(new
绑定)? YES -->this
绑定的是new
创建的新对象 - 函数通过
call/apply/bind
(显示绑定)? YES -->this
绑定的是指定的对象 - 函数在某个上下文对象中调用(隐式绑定)? YES -->
this
绑定的是那个上下文对象 - 默认绑定,严格模式指向
undefined
,否则指向全局对象
ES6的箭头函数(词法作用域的this机制,规则之外)
箭头函数的this
机制不同于传统的this
机制,它采取的是另外一种机制,词法作用域的this
判定规则。
// 例子一
var name = '无名氏';
var myInfo = {
name: '以乐之名',
getName: () => {
console.log(this.name);
}
};
var getName = myInfo.getName;
window.getName(); // 无名氏
myInfo.getName(); // 无名氏
// myInfo是在全局环境定义的,因此根据词法作用域,this指向全局对象
// 例子二
var name = '无名氏';
var myInfo = {
name: '以乐之名',
say: () => {
setTimeout(() => {
console.log(this.name);
})
}
};
myInfo.say(); // 无名氏
// 箭头函数通过作用域链来逐层查找this,最终找到全局变量myInfo,this指向全局对象
// 例子三
var name = '无名氏';
var myInfo = {
name: '以乐之名',
say: function() {
setTimeout(() => {
console.log(this.name);
})
}
};
myInfo.say(); // 以乐之名
// 箭头函数找到say: function(){},因此this的作用域来自myInfo
TIPS: setTimeout/setInterval/alert
的调用者都是全局对象
"箭头函数的this
始终指向函数定义时的this
,而非执行(调用)时的this
。箭头函数中的this
必须通过作用域链一层一层向外查找,来确定this
指向。"
扩展:箭头函数的书写规则
1. 箭头函数只能用函数表达式,不能用函数声明式写法(不包括匿名函数)
// 函数表达式
const getName = (name) => { return 'myName: ' + name };
// 匿名函数
setTimeout((name) => {
console.log(name);
}, 1000)
2. 如果参数只有一个,可不加括号()
;如果没有参数或多个参数需加括号()
// 只有一个参数
const getName = name => {
return `myName: ${name}`;
}
// 无参数
const getName = () => {
return 'myName: "以乐之名"';
}
// 多参数
const getName = (firstName, lastName) => {
return `myName: ${firstName} ${lastName}`;
}
3. 函数体只有一个可不加花括号{}
const getName = name => return `myName: ${name}`;
4. 函数体没有花括号{}
,可不写return,会自动返回
const getName = name => `myName: ${name}`;
参考文档:
本文首发Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。
前端进击的巨人(六):知否知否,须知this的更多相关文章
- 技本功丨知否知否,Redux源码竟如此意味深长(上集)
夫 子 说 元月二号欠下袋鼠云技术公号一篇关于Redux源码解读的文章,转眼月底,期间常被“债主”上门催债.由于年底项目工期比较紧,于是债务就这样被利滚利.但是好在这段时间有点闲暇,于是赶紧把这篇文章 ...
- [推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼、百战不殆)
原文:[推荐]ORACLE PL/SQL编程之五:异常错误处理(知已知彼.百战不殆) [推荐]ORACLE PL/SQL编程之五: 异常错误处理(知已知彼.百战不殆) 继上三篇:ORACLE PL/S ...
- Appium+python自动化(十六)- ADB命令,知否知否,应是必知必会(超详解)
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态. adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或 ...
- 《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(15)-Fiddler弱网测试,知否知否,应是必知必会
1.简介 现在这个时代已经属于流量时代,用户对于App或者小程序之类的操作界面的数据和交互的要求也越来越高.对于测试人员弱网测试也是需要考验自己专业技术能力的一种技能.一个合格的测试人员,需要额外关注 ...
- 技本功丨知否知否,Redux源码竟如此意味深长(下集)
上集回顾 Redux是如何使用的?首先再来回顾一下这个使用demo(谁让这段代码完整地展示了redux的使用) 如果有小伙伴对这段代码不是很理解的话,建议先去学习Redux的使用再来看这篇源码,这样更 ...
- 知否知否,VS Code 不止开源
VS Code, 昨夜始于“开源”,如今“开源”深处渡. 读者看到这句话,也许会有疑惑,为什么两个“开源”都加上了双引号? 其实是笔者有意为之,因为这个两个“开源”的意义有着很大的差别,第一个“开源” ...
- 前端进击的巨人(五):学会函数柯里化(curry)
柯里化(Curring, 以逻辑学家Haskell Curry命名) 写在开头 柯里化理解的基础来源于我们前几篇文章构建的知识,如果还未能掌握闭包,建议回阅前文. 代码例子会用到 apply/call ...
- EasyDSS高性能流媒体服务器前端重构(六)- webpack-dev-server 支持手机端访问
很多时候,前端开发的页面,不仅要在PC端测试效果, 还要在手机端测试效果. 在开发阶段, 我们以 webpack-dev-server 来启动浏览器, 打开正在开发的页面. webpack-dev-s ...
- 前端基础(六):Bootstrap框架
Bootstrap介绍 Bootstrap是Twitter开源的基于HTML.CSS.JavaScript的前端框架. 它是为实现快速开发Web应用程序而设计的一套前端工具包. 它支持响应式布局,并且 ...
随机推荐
- 529. Minesweeper扫雷游戏
[抄题]: Let's play the minesweeper game (Wikipedia, online game)! You are given a 2D char matrix repre ...
- mybatis pagehelper分页插件使用
使用过mybatis的人都知道,mybatis本身就很小且简单,sql写在xml里,统一管理和优化.缺点当然也有,比如我们使用过程中,要使用到分页,如果用最原始的方式的话,1.查询分页数据,2.获取分 ...
- node.js中 express + multer 处理文件上传
multer中间件,可以很方便的结合express处理用户表单上传的文件. 一.安装multer npm install multer 二.处理单个文件上传 const express = requi ...
- 校验金额、大小写字母、大写字母、合法uri、email
/* 合法uri*/ export function validURL(url) { const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0- ...
- est-framework框架的基本组件
rest-framework框架的基本组件 快速实例 Quickstart 序列化 创建一个序列化类 简单使用 开发我们的Web API的第一件事是为我们的Web API提供一种将代码片段实例序列 ...
- Aria2+WebUI,迅雷倒下之后的代替品
Aria2+WebUI,迅雷倒下之后的代替品 (2017-07-24 12:56:28) 转载▼ 分类: 软件 最近迅雷越来越作死了,砍第三方远程下载,强推迅雷9喂用户的屎,下载资源能砍就砍,以前 ...
- spring boot + spring batch 读数据库文件写入文本文件&读文本文件写入数据库
好久没有写博客,换了一家新公司,原来的公司用的是spring,现在这家公司用的是spring boot.然后,项目组布置了一个任务,关于两个数据库之间的表同步,我首先想到的就是spring batch ...
- BZOJ2212或洛谷3521 [POI2011]ROT-Tree Rotations
BZOJ原题链接 洛谷原题链接 线段树合并裸题. 因为交换子树只会对子树内部的逆序对产生影响,所以我们计算交换前的逆序对个数和交换后的个数,取\(\min\)即可. 对每个叶子节点建一棵动态开点线段树 ...
- Python3基础知识之日期时间与字符的转换
问题:“猿类”们都知道,编程中都会涉及到日期.时间类型与字符串类型的转换.不同场景,需要将字符串格式转换为日期类型:也需要将日期类型转换为字符串格式. 目标: 学习和积累python中time和dat ...
- Chapter3_操作符_别名机制
Java中的别名机制实际体现的是对于“=”这一类赋值操作符的使用规则和内涵.“=”的实际内涵是指将右边的变量的值(对于基本数据类型而言)或者某一个对象的引用(对于某个具体对象而言)复制到左边的变量名所 ...