前端进击的巨人(六):知否知否,须知this

45次阅读

共计 5220 个字符,预计需要花费 14 分钟才能阅读完成。

常见 this 的误解

指向函数自身(源于 this 英文意思的误解)
指向函数的词法作用域(部分情况)

this 的应用环境
全局环境
无论是否在严格模式下,全局执行环境中(任何函数体外部)this 都指向全局对象
var name = ‘ 以乐之名 ’;
this.name; // 以乐之名
函数(运行内)环境
函数内部,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.apply(thisArg, arg1, arg2, …)

var numbers = [5, 6, 2, 3, 7];

// 求 numbers 的最大值

// apply
var max = Math.max.apply(null, numbers);

// call
var max = Math.max.apply(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 指向。”
扩展:箭头函数的书写规则
箭头函数只能用函数表达式,不能用函数声明式写法(不包括匿名函数)
// 函数表达式
const getName = (name) => {return ‘myName: ‘ + name};

// 匿名函数
setTimeout((name) => {
console.log(name);
}, 1000)
如果参数只有一个,可不加括号 ();如果没有参数或多个参数需加括号 ()

// 只有一个参数
const getName = name => {
return `myName: ${name}`;
}

// 无参数
const getName = () => {
return ‘myName: ‘ 以乐之名‘;
}

// 多参数
const getName = (firstName, lastName) => {
return `myName: ${firstName} ${lastName}`;
}
函数体只有一个可不加花括号 {}

const getName = name => return `myName: ${name}`;
函数体没有花括号 {},可不写 return,会自动返回
const getName = name => `myName: ${name}`;

参考文档:

你不知道的 JavaScript(上卷)
彻底理解 js 中 this 的指向,不必硬背。
this|MDN

本文首发 Github,期待 Star!https://github.com/ZengLingYong/blog
作者:以乐之名本文原创,有不当的地方欢迎指出。转载请指明出处。

正文完
 0