JS你不知道的JavaScript笔记- this - 四种绑定规则 - 绑定优先级 - 绑定例外 - 箭头函数
Posted YK菌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS你不知道的JavaScript笔记- this - 四种绑定规则 - 绑定优先级 - 绑定例外 - 箭头函数相关的知识,希望对你有一定的参考价值。
今天继续总结《你不知道的javascript》,来探索探索JavaScript中的
this
关键字
我们之前学习作用域的时候提到过this
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
其实不止JavaScript中有this
关键字,像java等很多语言也都有this
这个关键字。
this
是一个很特别的关键字,被自动定义在所有函数的作用域中。
this
的不明确性让他成为了一个很令人头疼的东西,我们先来了解为什么要用this
。
1. 为什么要用 this
首先我们假设有两个对象分别代表两个人
let me = {
name: "yk",
};
let you = {
name: "YK菌",
};
我们要定义一个函数,函数是用来自我介绍的,没有 this
的话,我们通过传入不同的参数,来实现不同的自我介绍
function speak(context) {
console.log(`你好,我是${context.name}`);
}
我们这样调用函数,来传入参数
speak(me); // 你好,我是yk
speak(you); // 你好,我是YK菌
而如果我们使用this 的话,函数就可以这样来定义
function speak() {
console.log(`你好,我是${this.name}`);
}
这样来调用函数
speak.call(me); // 你好,我是yk
speak.call(you); // 你好,我是YK菌
所以说,this
提供了一种更优雅的方式来隐式传递一个对象引用,因此可以将API设计得更加简洁并且易于复用。
2. 关于this
的误解
2.1 this
不是指向函数自身
在函数中用this,在英语的语法中,this总是说的是自己,然而在函数中的this不是指向的函数自身,这一定要注意区别!!!
如果要从函数对象内部引用它自身,那只使用this是不够的。一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它。
function foo() {
foo.count = 1;
}
2.2 this
不指向函数的词法作用域
另外在上一篇博文中说到,JavaScript代码执行遵守的是词法作用域,但是this在任何情况下都不指向函数的词法作用域。
当一个函数被调用时,会创建一个执行上下文。
这个执行上下文会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。
this就是这个上下文的一个属性,会在函数执行的过程中用到。
所以说,this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
3. 什么是调用栈与调用位置
调用位置就是函数在代码中被调用的位置(而不是声明的位置)
这句话看上去很简单,甚至让人觉得是一句废话。但事实上,在有些编程模式下隐藏了真正的调用位置,让你不容易判断调用位置真的在哪里
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。
我们关心的调用位置就在当前正在执行的函数的前一个调用中。
function fun1() {
// 当前调用栈:fun1
// 当前调用位置:全局作用域
console.log("fun1");
fun2(); // fun2的调用位置:fun1
}
function fun2() {
// 当前调用栈是 fun1 -> fun2
// 当前调用位置:fun1
console.log("fun2");
fun3();
}
function fun3() {
// 当前调用栈是 fun1 -> fun2 -> fun3
// 当前调用位置:fun2
console.log("fun3");
}
fun1(); // fun1的调用位置:全局作用域
在fun3的第一行打一个断点,通过调试工具可以看到当前的调用堆栈和 this 的值
4. this
的绑定规则
知道了调用栈之后,我们就需要知道在函数的执行过程中调用位置如何决定this
的绑定对象
找到调用位置之后,根据下面四种绑定的规则来确定this的绑定
4.1 默认绑定 fun()
函数调用类型:独立函数调用
默认绑定:无法应用其他规则时的默认规则
函数调用时应用了this的默认绑定,因此this指向全局对象 (node中是global对象,浏览器中是window对象)
function foo() {
console.log(this);
console.log(this.a);
}
var a = 2
foo();
// Window {window: Window, self: Window, document: document, name: "", location: Location, …}
// 2
如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此 this 会绑定到 undefined :
function foo() {
'use strict'
console.log(this);
}
foo(); // undefined
对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
4.2 隐式绑定 obj.fun()
当函数引用有上下文对象(函数是否被某个对象拥有或者包含)时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
function foo() {
console.log(this);
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
obj.foo();
无论是直接在obj中定义foo 还是 先定义foo再添加为引用属性,这个函数严格来说都不属于obj对象
调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo() {
console.log(this); // {a:2, foo:f}
console.log(this.a); // 2
}
var obj = {
a: 2,
foo: foo,
};
var yk = {
a: 222,
obj: obj,
};
yk.obj.foo();
隐式绑定丢失的情况
这种隐式绑定有时候是会丢失的,我们来看下面这种情况
① 函数别名
function fun1() {
console.log(this);
console.log(this.a);
}
var obj = {
a: "局部参数",
fun1: fun1,
};
var fun2 = obj.fun1; // 函数别名
var a = "全局参数";
obj.fun1(); // 局部参数
fun2(); // 全局参数 【所以说this是在调用时绑定的,不是在定义的时候绑定的】
虽然
fun2
是obj.fun1
的一个引用,但是实际上,它引用的是fun1
函数本身,因此下面调用的fun2()
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
② 参数传递函数
function fun1() {
console.log(this);
console.log(this.a);
}
function fun2(fn) {
// obj.fun1传进来的是引用值,实际上就是fun1
fn(); // 直接调用,指向window
}
var obj = {
a: "局部参数",
fun1: fun1,
};
var a = "全局参数";
fun2(obj.fun1); // 全局参数
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。
这也就解释了为什么使用JavaScript环境中内置的setTimeout()函数中回调的this
指向的是全局对象了
在回调函数中丢失this
是非常常见的现象
setTimeout(obj.fun1, 100) // '全局对象'
在setTimeout内部是这样调用 回调函数 的
function setTimeout(fn, delay) {
// 等待dealy毫秒
fn(); // 调用位置 【obj.fun1是引用值,传进来的就是fun1,直接调用,指向window】
}
③ 事件处理器
还有一种情况
this
的行为会出乎我们意料:调用回调函数的函数可能会修改this
。
在一些流行的JavaScript库中事件处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。
无论是哪种情况,
this
的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制调用位置以得到期望的绑定。
4. 3 显式绑定 fun.call(obj)
在Function的原型对象上有三个方法apply、call、bind可以显式的改变this的指向
【JS】函数定义与调用方式-函数this指向问题-call-apply-bind方法使用与自定义
先看看 apply
和 call
显式绑定
function fun1() {
console.log(this);
console.log(this.a);
}
var obj = {
a: "局部对象",
};
fun1.call(obj); // 局部对象
显式绑定仍然无法解决我们之前提出的丢失绑定问题
这是因为显式绑定,会立即执行这个函数,回调函数中函数的执行时间是不确定的,所有我们需要提前将this绑定到指定的对象上,在需要的时候调用回调函数时,this是明确的。
显式强制绑定(硬绑定)就是解决这个问题的
① 显式强制绑定 —— 硬绑定 bind
创建函数fun2()
,并在它的内部手动调用了fun1.call(obj)
,因此强制把fun1
的this
绑定到了obj
。
无论之后如何调用函数fun2
,它总会手动在obj
上调用fun1
。
这种绑定是一种显式的强制绑定,因此我们称之为硬绑定
function fun1() {
console.log(this);
console.log(this.a);
}
var obj = {
a: "局部对象",
};
function fun2() {
fun1.call(obj); // 显式绑定
}
fun2(); // 局部对象 【内部进行了绑定】
setTimeout(fun2, 100); // 局部对象
// 硬绑定后的fun2不能再修改this
fun2.call(window); // 局部对象
② 硬绑定应用场景
① 创建一个包裹函数,负责接收参数并返回值
function fun1(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2,
};
// 创建一个包裹函数,负责接收参数并返回值
function fun2() {
return fun1.apply(obj, arguments);
}
var b = fun2(3); // 2 3
console.log(b); // 5
② 创建一个可以重复使用的 辅助绑定函数
function fun1(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 fun2 = bind(fun1, obj);
var b = fun2(3); // 2 3
console.log(b); // 5
其实这个辅助绑定函数,JavaScript已经帮我们创建好了就是函数原型上的bind()
方法
function fun1(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2,
};
// 使用 bind 方法
var fun2 = fun1.bind(obj);
var b = fun2(3); // 2 3
console.log(b); // 5
③ API调用的上下文
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和
bind(..)
一样,确保你的回调函数使用指定的this
function fun(el) {
console.log(el, this.id);
}
var obj = {
id: 'yk'
}
// 第二个参数用来指定this
[1,2,3].forEach(fun, obj); // 1 yk 2 yk 3 yk
这些API在内部实现了显式绑定
4.4 new
绑定
JavaScript中的new的机制和面向类的语言完全不同
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。
它们并不会属于某个类,也不会实例化一个类。
实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已
JavaScript中的所有的函数都是可以用new来调用,称为构造函数调用
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[Prototype]]连接【隐式原型 指向 构造函数的显式原型】
- 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function fun(a){
this.a = a;
}
// 将fun构造函数中的 this 绑定到obj
var obj = new fun(2)
console.log(obj.a) // 2
自定义new
/**
* 自定义new
* 创建Fn构造函数的实例对象
* @param {Function} Fn
* @param {...any} args
* @returns
*/
export default function newInstance(Fn, ...args) {
// 1. 创建新对象
// 创建空的object实例对象,作为Fn的实例对象
const obj = {};
// 修改新对象的原型对象
// 将Fn的prototype(显式原型)属性赋值给obj的__proto__(隐式原型)属性
obj.__proto__ = Fn.prototype;
// 2. 修改函数内部this指向新对象,并执行
//
const result = Fn.call(obj, ...args);
// 3. 返回新对象
// return obj
// 与new保持一直,如果构造函数有返回值,返回值是对象a就返回对象a,否则返回实例对象
return result instanceof Object ? result : obj;
}
根据上面的四条绑定规则,只要我们找到函数的调用位置,判断使用哪种规则,就可以知道this到底绑定给谁了
如果有多条绑定规则都满足,那就要看他们之间的优先级了
4.5 绑定的优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定(最低)
① 显式绑定 > 隐式绑定
function fun() {
console.log(this.a);
console.log(this);
}
let obj1 = {
a: "obj1里面的a",
fun: fun,
};
let obj2 = {
a: "obj2里面的a",
fun: fun,
};
obj1.fun(); // 隐式绑定 obj1
fun.call(obj2); // 显式绑定 obj2
// 比较优先级
obj1.fun.call(obj2); // obj2
② new绑定 > 隐式绑定
function fun(a) {
this.a = a;
}
let obj1 = {
fun: fun,
};
obj1.fun("隐式绑定");
console.log(obj1.a); // "隐式绑定"
let obj2 = new fun("new绑定");
console.log(obj2.a); // "new绑定"
// 比较优先级
let obj3 = new obj1.fun("new绑定");
console.log(obj1.a); // "隐式绑定"
console.log(obj3.a); // "new绑定"
③ new绑定 > 显式绑定
function fun(a) {
this.a = a;
}
let obj1 = {};
let fun1 = fun.bind(obj1);
fun1("硬绑定的a");
console.log(obj1.a); // 硬绑定的a
let fun2 = new fun1("new绑定的a");
console.log(obj1.a); // 硬绑定的a
console.log(fun2.a); // new绑定的a
4.6 规则总结
① 由new调用?绑定到新创建的对象。
② 由call或者apply(或者bind)调用?绑定到指定的对象。
③ 由上下文对象调用?绑定到那个上下文对象。
④ 默认:在严格模式下绑定到undefined,否则绑定到全局对象。
5. 绑定例外
5.1 显式绑定时传入null
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则
用处
function fun(a, b) {
console.log(`a:${a}, b:${b}`);
}
// 将数组展开成参数【ES6可以使用展开运算符】
fun.apply(null, [2, 3]); // a:2, b:3
// 函数柯里化
let fun1 = fun.bind(null, 2);
fun1(3); // a:2, b:3
总是传null也不太好,可以传一个空对象 ø ø ø
function fun(a, b) {
console.log(`a:${a}, b:${b}`);
}
let ø = Object.create(null);
// 将以上是关于JS你不知道的JavaScript笔记- this - 四种绑定规则 - 绑定优先级 - 绑定例外 - 箭头函数的主要内容,如果未能解决你的问题,请参考以下文章
读书笔记《你不知道的JavaScript(上卷)》——第二部分 this和对象原型
你不知道的Javascript(上卷)读书笔记之一 ---- 作用域