js你不知道的javascript笔记-this-四种绑定规则-绑定优先级-绑定例外-箭头函数(代码片段)

YK菌 YK菌     2022-12-19     661

关键词:

今天继续总结《你不知道的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是在调用时绑定的,不是在定义的时候绑定的】

虽然fun2obj.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方法使用与自定义


先看看 applycall 显式绑定

function fun1() 
  console.log(this);
  console.log(this.a);


var obj = 
  a: "局部对象",
;

fun1.call(obj); // 局部对象

显式绑定仍然无法解决我们之前提出的丢失绑定问题

这是因为显式绑定,会立即执行这个函数,回调函数中函数的执行时间是不确定的,所有我们需要提前将this绑定到指定的对象上,在需要的时候调用回调函数时,this是明确的。

显式强制绑定(硬绑定)就是解决这个问题的

① 显式强制绑定 —— 硬绑定 bind

创建函数fun2(),并在它的内部手动调用了fun1.call(obj),因此强制fun1this绑定到了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来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会被执行[[Prototype]]连接【隐式原型 指向 构造函数的显式原型】
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么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);

// 将

你不知道的javascript笔记

this和对象原型this是一个很特别的关键字,被自动定义在所有函数的作用域中//foo.count是0,字面理解是错误的    functionfoo(num){        console.log("foo:"+num);    &n 查看详情

《你不知道的javascript》——this和对象原型

 《你不知道的javascript》【3】——this和对象原型https://www.bilibili.com/video/BV1iE411P7UP 浅显的总结《你不知道的js》this指向          右查找的副作用:查找到顶层都找不到,就会抛出 查看详情

你不知道的javascript-上卷の读书笔记

...;        1— 作用域对JavaScript而言,大部分情况下编译发生 查看详情

读书笔记《你不知道的javascript(上卷)》——第二部分this和对象原型(代码片段)

文章目录第6章行为委托6.1面向委托的设计6.1.1类理论6.1.2委托理论1.互相委托(禁止)2.调试6.1.3比较思维模型6.2类与对象6.2.1控件“类”ES6的class语法糖6.2.2委托控件对象6.3更简洁的设计反类6.4更好的语法反词法6... 查看详情

你不知道的javascript(上卷)读书笔记之一----作用域

你不知道的Javascript(上卷)这本书在我看来是一本还不错的书籍,这本书用比较简洁的语言来描述Js的那些”坑”,在这里写一些博客记录一下笔记以便消化吸收。1编译原理在此书中,开始便提出:Javascript是一门编译型语言,我... 查看详情

《你不知道的javascript》整理——this

最近在读一本进阶的JavaScript的书《你不知道的JavaScript(上卷)》,这次研究了一下“this”。当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、... 查看详情

你不知道的javascript(this)

 对this的常见误解  this指向函数本身;  this指向函数的词法作用域;this是在运行时进行绑定的,并不是在编写时,它的上下文取决于函数调用时的条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用... 查看详情

javascript中的this-笔记

...,找了一些别人的博客看,又重新看了一下《你不知道的JavaScript》,感觉基本上是弄懂了,挑一些重点的地方记录一下,有些地方对我来说书上解释写的不够多,所以自己做下补充以方便理解,有理解错的地方还望指出。 ... 查看详情

读书笔记-你不知道的js上-对象

好想要对象···   函数的调用位置不同会造成this绑定对象不同。但是对象到底是什么,为什么要绑定他们呢?(可以可以,我也不太懂)  语法  对象声明有两个形式:  1、字面量=>varobj={...};  2、构造形式=&g... 查看详情

你不知道的javascript(中卷)笔记

<!DOCTYPEhtml><html><head><metacharset="utf-8"><title>你不知道的javascript(中卷)</title></head><body><scripttype="text/javascript">/*//封装对象包装vara=newBool 查看详情

你不知道的javascript(上卷卷)笔记

<!DOCTYPEhtml><html><head><metacharset="utf-8"><title>你不知道的javascript(上卷)</title></head><body></body></html>   查看详情

你不知道的javascript笔记

类型:JavaScript有7种内置类型空值(null)未定义(undefined)布尔值(boolean)数字(number)字符串(string)对象(object)符号(symbol)   除对象以外,其他统称为“基本类型” 用typeof运算符来查看值的类型typeofundefined ==="undefi... 查看详情

你不知道的javascript(上卷)读书笔记之二----词法作用域

...要的工作类型,一种是词法作用域,一种是动态作用域,Javascript采用的是词法作用域,关于动态作用域的有兴趣的可以自行Google。1.词法阶段         首 查看详情

你不知道的javascript笔记

...算符则相当于标点符号和连接词。          JavaScript中表达式可以返回一个结果值。            vara=3*6;            varb=a;              b; &nbs 查看详情

你不知道的javascript中,读书笔记

七种内置类型null,undefined,boolean,number,string,object,symboltypeofnull===‘object‘//truenull是typeof是object的唯一的假值typeoffunction会返回‘function‘使用typeofx!==‘undefined‘比直接判断x更加安全,因为不会引发referenceerror 查看详情

你不知道的javascript笔记

规避冲突functionfoo(){functionbar(a){i=3;console.log(a+i);}for(vari=0;i<10;i++){bar(i*2)}} //11无限死循环 区分函数声明和函数表达式最简单的方法是看function关键字出现的位置,如果function是声明中的第一个词,那么是函数声明,否则是... 查看详情

你不知道的javascript--上卷--读书笔记1

作用域是什么?  答:在《你不知道的javascript》书中提到,作用域就是根据名称查找变量的一套规则。古语有“无规矩不成方圆”,但是没有方圆,规矩又给谁用?所以个人理解作用域就是“规矩”+”方圆“。作用域是在创... 查看详情

你不知道的javascript--上卷--读书笔记2

...以访问它们被定义时所处的作用域中的任何变量,这就是JavaScript的闭包。闭包有哪些应用?  答:函数作为返回值:functionfoo(){vara=2;functionbar(){//bar拥有涵盖foo作用域的闭包,并对它保持 查看详情