[toc]
this 到底指向谁?有一种广泛流传的说法是:*谁调用它,this 就指向谁。*
也就是说,this 的指向实在调用时确定的。这么说没有太大的问题,可是并不全面。面试官要求我们用更加规范的语言进行总结,那么他到底再等什么样的回答呢?
事实上,调用函数会创建新的属于函数自身的执行上下文。执行上下文的调用创建阶段会决定 this 的指向。到此,我们可以得出一个结论:
> this 的指向,是在调用函数时根据执行上下文所动态确定的。
具体环节和规则,可以先“死记硬背”以下几条规律,后面来慢慢一一分析:
* 在函数体中,简单调用函数时(非显示/隐式绑定下),严格模式下 this 绑定到undefined,否则绑定到全局对象 window/global;
* 一般构造函数new调用,绑定到新创建的对象上;
* 一般由 bind/call/apply 方法显示调用,绑定到指定参数的对象上;
* 一般由上下文对象调用,绑定再该对象上;
* 箭头函数中,根据外层上下文绑定的this决定this的指向。
当然,真实环境多样,我们来逐一梳理。
## 全局环境下的 this
在这种情况相对简单直接,函数在浏览器全局环境中被简单调用,非严格模式下 this 指向window;在 use strict 指明严格模式的情况下就是 undefined。我们来看例题,请描述打印结果:
```javascript
function f1() {
console.log(this)
}
function f2() {
'use strict'
console.log(this)
}
f1() // window
f2() // undefined
```
这样的题目比较基础,但是需要候选人格外注意其变种,请再看一题:
```javascript
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
var fn1 = foo.fn
fn1()
```
这里的 this 任然指向的是 window。虽然 fn 函数在 foo 对象中作为方法被引用,但是在赋值给 fn1 之后,fn1 的执行任然是在 window 的全局环境中。因此输出 window 和 undefined,他们相当于:
```javascript
console.log(window)
console.log(window.bar)
```
还是上面这道题目,如果调用改变为:
```javascript
const foo = {
bar: 10,
fn: function() {
console.log(this)
console.log(this.bar)
}
}
foo.fn()
```
将会输出:
```javascript
{ bar: 10, fn: f }
10
```
因为这个时候 this 指向的时最后调用它的对象,在 foo.fn() 语句中 this 指向 foo 对象。请记住:
在执行函数时,如果函数中的 this 是被上一级的对象所调用,那么 this 指向的就是上一级对象;否则指向全局环境。
## 上下文对象调用中的this
如上结论,面对下面的题目的时候我们就不会在困惑了:
```javascript
const student = {
name: 'zhangsan',
fn: function() {
return this
}
}
console.log(student.fn() === student) // true
```
最终结果将返回 true。
当存在更复杂的调用关系时,请看例题:
```javascript
const person = {
name: 'zhangsan',
brother: {
name: 'lisi',
fn: function() {
return this.name
}
}
}
console.log(person.brother.fn())
```
在这种嵌套关系中,this 指向最后调用它的对象,因此输出将会时:lisi。
到此,this 的上下文对象调用已经理解得比较清楚了。当我们再看一道更高阶的题目:
```javascript
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn()) // o1
console.log(o2.fn()) // o1
console.log(o3.fn()) // undefined
```
答案是:o1、o1、undefined,你对对了吗?
我们来一一分析:
* 第一个 console 最简单,o1 没有问题。难点在第二个和第三个上面,关键还是看调用this的哪个函数。
* 第二个 console 的 o2.fn(),最终还是调用 o1.fn(),因此答案当然时 undefined。
* 最后一个,在进行 var fn = o1.fn 赋值之后,时“裸奔”调用,因此这里的 this 指向 window,答案当然时undefined。
如果面试者回答顺利,可以紧接着追问,如果我们需要让:
```javascript
console.log(o2.fn())
```
输出 o2 该怎么做?
一般开发者可能会想到使用 bind/call/apply 来对 this的指向进行干预,这确实时一种思路。但是我接着问,如果不能使用 bind/call/apply,有别的办法吗?
这样可以考察候选人基础掌握的深度及随机应变的能力。答案为:
```javascript
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn()) // o2
```
还是应用那个最重要的结论:this 指向最后调用它的对象,在 fn 执行时,挂到 o2 对象上即可,我们提前进行了赋值操作。
## bind/call/apply 改变 this 指向
上文提到 bind/call/apply,在这个概念上,比较常见的基础考察点时:bind/call/apply 三个方法的区别。
这样的问题相对基础,我们直接上答案:一句话总结,**他们都是用来改变相关函数的this指向的,但是 直接 call/apply 是直接进行相关函数调用;bind 不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this指向,开发者需要手动调用即可。** 再具体的 call/apply 之间的区别主要体现在参数设定上。
用代码来总结:
```javascript
const target = {}
fn.call(target, 'arg1', 'arg2')
```
相当于
```javascript
const target = {}
fn.apply(target, ['arg1', 'arg2'])
```
相当于
```javascript
const target = {}
fn.bind(target, 'arg1', 'arg2')()
```
具体基础用法这里不再科普,如果读者尚不清楚,需要自己补充一下知识点。
我们来看一道例题:
```javascript
const foo = {
name: 'zhangsan',
logName: function() {
console.log(this.name)
}
}
const bar = {
name: 'lisi',
}
console.log(foo.logName.call(bar)) // lisi
```
将会输出 lisi,这不难理解。但是对 bind/call/apply 的高级考察往往会结合构造函数以及组合式实现继承。构造函数的使用案例,我们结合下面的例题进行分析。
## 构造函数和this
这方面最直接的例题为:
```javascript
function Foo() {
this.bar = 'zhangsan'
}
const instance = new Foo()
console.log(instance.bar) // zhangsan
```
答案将是zhangsan。但是这样的场景往往伴随这下一个问题:new 操作符调用构造函数,具体做了什么?以下供参考:
- 创建一个对象;
- 将构造函数的this指向这个新对象;
- 为这个对象添加属性、方法等;
- 最终返回新对象。
以上过程,也可以用代码表述:
```javascript
var obj = {}
obj.__proto__ = Foo.prototype
Foo.call(obj)
```
当然,这里对 new 的模拟是一个简单基本版本。
需要指出的是,如果在构造函数中出现了显示 return 的情况,那么需要注意分为两种场景:
```javascript
function Foo() {
this.user = 'zhangsan'
const o = {}
return o
}
const instance = new Foo()
console.log(instance.user) // undefined
```
将会输出undefined,此时 instance 是返回的空对象 o。
```javascript
function Foo() {
this.user = 'zhangsan'
return 1
}
const instance = new Foo()
console.log(instance.user) //zhangsan
```
将会输出zhangsan,也就是说此时 instance 是返回目标对象示例 this。
**结论:如果构造函数中显示返回一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this仍然指向实例。**
## 箭头函数中的this指向
**箭头函数使用 `this` 不适应以上标准规则,而是根据外层(函数或者全局)上下文来决定**。
来看题目:
```javascript
const foo = {
fn: function() {
setTimeout(function(){
console.log(this)
})
}
}
console.log(foo.fn()) // window
```
这道题中,`this` 出现在 `setTimeout()` 中的匿名函数里,因此 `this` 指向 window 对象。如果需要 `this` 指向 foo 这个 object 对象,可以巧用箭头函数解决:
```javascript
const foo = {
fn: function() {
setTimeout(() => {
console.log(this)
})
}
}
console.log(foo.fn())
// { fn: f }
```
单纯箭头函数中的 `this` 非常简单,但是综合所有情况,结合 `this` 的优先级考察,这是哈哈 `this` 指向并不好确定。
## this 优先级相关
我们常常把通过 call、apply、bind、new 对 `this` 绑定的情况称为显示绑定;根据调用关系确定的 `this` 指向称为隐式绑定。
那么显示绑定和隐式绑定谁的优先级更高呢?
情况例题:
```javascript
function foo(a) {
console.log(this.a)
}
const obj1 = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
```
输出分别为2、1,也就是说 call、apply 的显示绑定一般来说优先级更高。
```javascript
function foo(a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
```
上述代码通过 `bin` 将 bar 函数中的 `this` 绑定为 obj1 对象。执行 `bar(2) `后 `obj1.a` 的值为`2`。即经过 `bar(2)` 执行后,`obj1` 对象为: `{ a:2 }`。
当再使用 bar 作为构造函数时:
```javascript
var baz = new bar(3)
console.log(baz.a) // 3
```
将会输出3。我们看到 bar 函数本身式通过 `bind` 方法构造的函数,其内部已经对将 `this` 绑定为 obj1,它再作为构造函数,通过 `new` 调用时,返回的实例已经与 obj1 解绑。也就是说:
> `new` 绑定修改了 bind 绑定的 `this`,因此 `new` 绑定的优先级比显示 bind绑定更高
我们再看看:
```javascript
function foo() {
return a => {
console.log(this.a)
}
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2)) // 2
```
将会输出2。由于 `foo()` 的 this 绑定到 obj1,bar(引用箭头函数)的 `this` 也会绑定到 obj1,箭头函数的绑定无法被修改。
如果将 foo 完全写成箭头函数的形式:
```javascript
var a = 123
const foo = () => a => {
console.log(this.a)
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2)) // 123
```
将会输出123。
这里再“抖个机灵”,仅仅将上述代码的第一处变量 a 的赋值改为:
```javascript
const a = 123
/* 同上 */
```
答案将会输出 `undefined`,原因时因为使用 `const` 声明的变量不会挂载到 window 全局对象当中。
- 说明
- CSS与HTML
- BFC的特性及其常见应用
- CSS深入理解之margin
- CSS深入理解之line-height
- CSS盒模型相关知识
- CSS知识总结
- HTML知识总结
- 三栏布局五种方式
- JavaScript内置对象
- 1.循环
- 2.数组方法对比
- 3.字符串实用常操纪要
- JavaScript核心
- var、let、const定义变量
- this 的指向问题详解
- 箭头函数
- ES6部分知识归纳
- ES6的Class
- Promise和Async/await
- 面向对象的概念及JS中的表现
- 创建对象的九种方式
- JS的继承
- 闭包总结
- 构造函数与作用域
- 原型与原型链
- 函数的四种调用模式
- apply、call、bind详解
- JavaScript应用
- 1.JavaScript实现深拷贝与浅拷贝
- 2.函数防抖与节流
- 3.无阻塞脚本加载技术
- DOM
- 如何写出高性能DOM?
- 事件探秘
- 事件委托
- 操作DOM常用API详解
- 重排和重绘
- 运行机制与V8
- 浏览器的线程和进程
- Vue.js
- Vue.js知识点总结
- Vue-Router知识点总结
- 父子组件之间通信的十种方式
- 优化首屏加载
- 关于Vuex
- 前端路由原理及实现
- 在Vue.js编写更好的v-for循环的6种技巧
- 12个Vue.js开发技巧和窍门
- 网络协议
- HTTP缓存机制
- UDP协议
- TCP协议
- HTTPS协议
- HTTPS的背景知识、协议的需求、设计的难点
- HTTPS与HTTP的区别
- 框架与架构
- MVC、MVP、MVVM
- Gulp与Webpack的区别
- Angular React 和 Vue的比较
- 虚拟DOM和实际的DOM有何不同?
- 架构问题
- 工程化
- npm link命令
- npm scripts 使用指南
- 前端工程简史
- 常见的构建工具及其对比
- Webpack基本配置与概念
- 设计模式
- 工厂设计模式
- 单例设计模式
- 适配器模式
- 装饰器模式