关键词:
今天继续总结《你不知道的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);
// 将你不知道的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作用域的闭包,并对它保持 查看详情