记录--一道字节面试题引出的this指向问题
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

var length = 10;
function fn () {
return this.length + 1;
}
var obj = {
length: 5,
test1: function () {
return fn();
}
}
obj.test2 = fn;
console.log(obj.test1()); // 11
console.log(fn() === obj.test2()); // false
看上面这段代码,这就是字节面试官准备的一道面试题,在面试过程中答得不是很好也没有完全做对。主要还是由于前两道题答得不是很好影响到后面答这道题时大脑是懵逼状态,再加上紧张让自己一时不知道这道题的考点是什么,进而影响到后续的面试状态。
其实这题不难,就是考JS中的基础:this的指向问题,也就是这篇文章要聊的主题。
this的原理
this指向的值总是取决于它的执行环境。执行环境是代码执行时当前的环境或者作用域,在运行时,JavaScript会维护一个执行环境栈;最顶部的那个是调用时所使用的执行环境,当执行环境变化时,this的值也会改变。
其实理解this的指向是非常重要的,至少它能让你在实际开发中少走弯路,少写bug,从而更好地掌握JavaScript这门语言。接下来将从一些栗子来介绍this在各个场景下的指向问题,便于更好地去理解this这个在面试中经常遇见的基础问题。
一、全局环境的this
在JavaScript中,如果this是在全局环境定义的,默认就是指向全局对象。而对于浏览器来说,全局对象就是window对象,所以this就是指向就是window对象。那如果是Node.js呢?由于Node.js没有window对象,所以this不能指向window对象;但是Node.js有global对象呀,也就是说在Node.js的全局作用域中this的指向将会是global对象。
console.log(this === window); // true

二、函数上下文调用
2.1 函数直接调用
普通函数内部的this指向有两种情况:严格模式和非严格模式。
非严格模式下,this默认指向全局对象,如下:
function fn1() {
console.log(this);
}
fn1(); // window
严格模式下,this为undefined。如下:
function fn2() {
"use strict"; // 严格模式
console.log(this);
}
fn1(); // undefined
2.2 对象中的this
在对象方法中,this的指向就是调用该方法的所在对象。此时的this可以访问到该方法所在对象下的任意属性,如下:
const name = '张三';
const obj = {
name: '李四',
fn: function() {
console.log(this);
console.log(this.name);
}
}
obj.fn();
代码中定义了一个对象,对象中有个fn()方法,调用这个方法打印结果如下:

可以看到,打印出来的this的值为obj这个对象,也就是fn()所在的对象,进而此时的this就能访问到该对象的所有属性。
如果fn()方法中返回的是一个匿名函数,在匿名函数中访问this.name,结果是什么呢?
const name = '张三';
const obj = {
name: '李四',
fn: function() {
return function() {
console.log(this);
console.log(this.name);
}
}
}
const fn = obj.fn();
fn();
匿名函数的执行环境是全局作用域,那这里this打印出来的值就是window对象,而this.name则是window对象下的name属性,即this.name的值为'张三'。结果如下图:
2.3 原型链中this
原型链中的this指向也是指向调用它的对象,如下:
const obj = {
fn: function() {
return this.a + this.b;
}
}
const obj1 = Object.create(obj);
obj1.a = 1;
obj1.b = 2;
obj1.fn(); // 3
可以看到,obj1中没有属性fn,当执行obj1.fn()时,会去查找obj1的原型链,找到fn()方法后就执行代码,这与函数内部this指向对象obj1没有任何关系。但是这里的this指向就是obj1,所以只需要记住谁调用就指向谁。
2.4 构造函数中的this
构造函数中,如果显式返回一个值并且返回的是个对象,那么this就指向这个返回的对象;如果返回的不是一个对象,那么this指向的就是实例对象。如下:
function fn() {
this.name = '张三';
const obj = {};
return obj;
}
const fn1 = new fn();
console.log(fn1); // {}
console.log(fn1.name); // undefined
上述代码将会打印结果为undefined,此时fn1是返回的对象obj,如下:

function fn() {
this.name = '张三';
return 1;
}
const fn1 = new fn();
console.log(fn1);
console.log(fn1.name);
上述代码则会打印结果为'张三',此时的fn1是返回的fn实例对象的this。

2.5 call/apply/bind改变this
call、apply、bind作用是改变函数执行时的上下文,也就是改变函数运行时的this指向。
2.5.1 call
call接受两个参数,第一个参数是this的指向,第二个参数是函数接收的参数,以参数列表形式传入。call改变this指向后原函数会立即执行,且只是临时改变this指向一次。
function fn(...args) {
console.log(this);
console.log(args);
}
let obj = {
name:"张三"
}
fn.call(obj, 1, 2);
fn(1, 2);
可以看到,call让this的指向变成了obj,而直接调用fn()方法this的指向是window对象。

如果把第一个参数改为null、undefined,那结果是怎样的呢?答案:this的指向是window对象。如下:
fn.call(null, 1, 2);
fn.call(undefined, 1, 2);

2.5.2 apply
apply第一个参数也是this的指向,第二个参数是以数组形式传入。同样apply改变this指向后原函数会立即执行,且只是临时改变this指向一次。
function fn(...args) {
console.log(this);
console.log(args);
}
let obj = {
name:"张三"
}
fn.apply(obj, [1, 2]);
fn([1, 2]);
可以看到,apply让this的指向变成了obj,而直接调用fn()方法this的指向是window对象。

同样,如果第一个参数为null、undefined,this默认指向window。

apply和call唯一的不同就是第二个参数的传入形式:apply需要数组形式,而call则是列表形式。
2.5.3 bind
bind第一参数也是this的指向,第二个参数需要参数列表形式(但是这个参数列表可以分多次传入)。bind改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。
function fn(...args) {
console.log(this);
console.log(args);
}
let obj = {
name:"张三"
}
const fn1 = fn.bind(obj);
fn1(1, 2);
fn(1, 2);

2.5.4 小结
从上面可以看到,apply、call、bind三者的区别在于:
- 都可以改变函数的
this对象指向; - 第一个参数都是
this要指向的对象,如果没有这个参数或参数为undefined或null,则默认指向window对象; - 都可以传参,
apply是数组,call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入; bind是返回绑定this之后的函数,apply、call则是立即执行。
三、箭头函数中的this
箭头函数本身没有this的,而是根据外层上下文作用域来决定的。箭头函数的this指向的是它在定义时所在的对象,而非执行时所在的对象,故箭头函数的this是固定不变的。还有就是无论箭头函数嵌套多少层,也只有一个this的存在,这个this就是外层代码块中的this。如下:
const name = '张三';
const obj = {
name: '李四',
fn: () => {
console.log(this); // window
console.log(this.name); // 打印李四?错的,是张三
}
}
obj.fn();
看上述代码,都会以为fn是绑定在obj对象上的,但其实它是绑定在window对象上的。为什么呢?
fn所在的作用域其实是最外层的js环境,因为没有其他函数的包裹,然后最外成的js环境指向的是window对象,故这里的this指向的就是window对象。
那要作何修改使它能永远指向obj对象呢?如下:
const name = '张三';
const obj = {
name: '李四',
fn: functon() {
let test = () => console.log(this.name); // 李四
return test; // 返回箭头函数
}
}
const fn1 = obj.fn();
fn1();
上述代码就能让其永远的绑定在obj对象上了,如果想用call来改变也是改变不了的,如下:
const obj1 = {
name: '王二'
}
fn1.call(obj1); // 李四
fn1.call(); // 李四
fn1.call(null, obj1); // 李四
最后是使用箭头函数需要注意以下几点:
- 不能使用
new构造函数 - 不绑定
arguments,用rest参数...解决 - 没有原型属性
- 箭头函数不能当做
Generator函数,不能使用yield关键字 - 箭头函数的
this永远指向其上下文的this,任何方法都改变不了其指向,如call,bind,apply
四、globalThis
来看看MDN是怎么描述globalThis的:
globalThis提供了一个标准的方式来获取不同环境下的全局this对象(也就是全局对象自身)。不像window或者self这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用globalThis,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的this就是globalThis。
尽管如此,还是要看看globalThis在现有各种浏览器上的兼容情况,如下图:

可以看到兼容情况还是可以,除了IE浏览器。。。
下面就演示实现一个手写call()方法,其中就使用到globalThis来获取全局this对象,如下:
function myCall(context, ...args) {
if (context === null) context = globalThis;
if (context !== 'object') context = new Object(context);
const key = symbol();
const res = context[key](...args);
delete context[key];
return res;
}
五、this的优先级
this的绑定规则有4种:默认绑定,隐式绑定,显式绑定和new绑定。那下面分别来说说:
5.1 默认绑定
var name = '张三';
function fn1() {
// 'use strict';
var name = '李四';
console.log(this.name); // 张三
}
fn1(); // window
默认绑定一般是函数直接调用。函数在全局环境调用执行时,this就代表全局对象Global,严格模式下就绑定的是undefined
5.2 隐式绑定
隐式绑定就是谁调用就是指向谁,如下:
const name = '张三';
const obj = {
name: '李四',
fn: function() {
console.log(this.name); // 李四
}
}
obj.fn(); // obj调用就是指向obj
接着看下面两个栗子,如下:
// 链式调用
const name = '张三';
const obj = {
name: '李四',
fn: function() {
console.log(this.name);
}
} const obj1 = {
name: '王二',
foo: obj
} const obj2 = {
name: '赵五',
foo: obj1
} obj2.foo.foo.fn(); // 打印:李四(指向obj,本质还是obj调用的)
再来看一个,就可以更清楚了,如下:
const name = '张三';
const obj = {
name: '李四',
fn: function() {
console.log(this.name);
}
} const obj1 = {
name: '王二',
foo: obj.fn
} obj1.foo(); // 打印:王二(指向obj1)
5.3 显式绑定
call、apply、bind对 this 绑定的情况就称为显式绑定。
const name = '张三';
const obj = {
name: '李四',
fn: function() {
console.log(this); // obj
console.log(this.name); // 李四
}
} const obj1 = {
name: '王二'
} obj.fn.call(obj1); // obj1 王二
obj.fn.apply(obj1); // obj1 王二
const fn1 = obj.fn.bind(obj1);
fn1(); // obj1 王二
5.4 new绑定
执行new操作的时候,将创建一个新的对象,并且将构造函数的this指向所创建的新对象。
function foo() {
this.name = '张三';
}
let obj = new foo();
console.log(obj.name); // 张三
其实new操作符会做以下工作:
- 创建一个新的对象
obj - 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this绑定到新建的对象obj上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
手动实现new代码,如下:
function myNew(fn, ...args) {
// 1.创建一个新对象
const obj = {};
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = fn.prototype;
// 3.将构建函数的this指向新对象
let result = fn.apply(obj, args);
// 4.根据返回值判断
return result instanceof Object ? result : obj;
}
测试一下,如下:
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.bark = function() {
console.log(this.name);
}
let dog = myNew(Animal, 'maybe', 4);
console.log(dog);
dog.bark();

5.5 小结
this的优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
至此,this的相关原理知识就介绍完了,现在转过头再去看字节的那道面试题是不是就可以轻松作答了。this是javascript中最基础的知识点,往往就是这些最基础的知识让我们在很多面试过程中一次次倒下,所以基础知识真的很有必要再去深入了解,这样不管是在以后的工作还是面试中都能够让我们自信十足,不再折戟沉沙。
本文转载于:
https://juejin.cn/post/7089690603755143199
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--一道字节面试题引出的this指向问题的更多相关文章
- 一道sql面试题(查询语句)
一道sql面试题(查询语句) id name age 1 a 11 2 b 11 3 c 12 4 d 13 5 e ...
- 一道 JavaScript 面试题
有一道 JavaScript 面试题. f = function () { return true; }; g = function () { return false; }; (function() ...
- 一道经典面试题-----setTimeout(function(){},0)
一道经典面试题-----setTimeout(function(){},0) 转载: http://www.w3cfuns.com/notes/17398/e8a1ce8f863e8b5abb5300 ...
- 一道Python面试题
无意间,看到这么一道Python面试题:以下代码将输出什么? def testFun(): temp = [lambda x : i*x for i in range(4)] return ...
- new与属性访问的顺序,从一道JS面试题说起
这段时间一直在研究设计模式,在看工厂模式的时候,看到一段代码 VehicleFactory.prototype.createVehicle = function ( options ) { if( o ...
- 记录一道神仙CTF-wtf.sh-150
记录一道完全超出我能力的CTF神仙题(不愧是世界级比赛的真题orz),此题我仅解出了第一部分的flag,第二部分则参考了WP.不得不说这种题目解出来还是很有自豪感的嘛~ 直接看题! 0x01 第一部 ...
- 从一道网易面试题浅谈 Tagged Pointer - darcy_tang 的博客
前言 这篇博客九月就想写了,因为赶项目拖了到现在,抓住17年尾巴写吧~ 正文 上次看了一篇 <从一道网易面试题浅谈OC线程安全> 的博客,主要内容是: 作者去网易面试,面试官出了一道面试题 ...
- 字节跳动的一道python面试题
#!/usr/bin/python #coding=utf-8 #好好学习,天天向上 lst = ['hongkong','xiantyk','chinat','guangdong','z'] lst ...
- why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。
这是why的第 65 篇原创文章 荒腔走板 大家好,我是 why,欢迎来到我连续周更优质原创文章的第 65 篇.老规矩,先荒腔走板聊聊技术之外的东西. 上面这图是去年的成都马拉松赛道上,摄影师抓拍的我 ...
- 关于fork的一道经典面试题
这是一道面试题,问程序最终输出几个“-”: #include<stdio.h> #include<sys/types.h> #include<unistd.h> i ...
随机推荐
- NebulaGraph入门介绍
NebulaGraph入门介绍 什么是图数据库? 图数据库就会是存储图形网络并能从中检索信息的数据库. 图数据库在处理关联关系上有极大的优势,它以图论为理论基础,使用图模型,将关联数据的实体作为顶点( ...
- 如何基于 spdlog 在编译期提供类 logrus 的日志接口
如何基于 spdlog 在编译期提供类 logrus 的日志接口 实现见 Github,代码简单,只有一个头文件. 前提 几年前看到戈君在知乎上的一篇文章,关于打印日志的一些经验总结: 实践下来很受用 ...
- AIR32F103(十二) 搭载 AIR32F103CBT6 的Bluepill核心板
目录 AIR32F103(一) 合宙AIR32F103CBT6开发板上手报告 AIR32F103(二) Linux环境和LibOpenCM3项目模板 AIR32F103(三) Linux环境基于标准外 ...
- 【Unity3D】点选物体、框选物体、绘制外边框
1 需求描述 绘制物体外框线条盒子 中介绍了绘制物体外框长方体的方法,本文将介绍物体投影到屏幕上的二维外框绘制方法. 点选物体:点击物体,可以选中物体,按住 Ctrl 追加选中,选中的物体设置为红 ...
- Spring源码之spring事务
目录 Spring事务 事务自定义标签 自定义标签 解析标签 bean 的初始化 InfrastructureAdvisorAutoProxyCreator 获取增强方法 获取所有增强中内适用于当前方 ...
- wsl 配置ubuntu apt源为阿里源
$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak $ sudo sed -i 's/security.ubuntu/mirrors.a ...
- 无 dmp 文件的情况下使用 windbg
项目线上出现了闪退问题,查看日志发现是 sentry 模块出问题了 因为用户电脑上没有 pdb 文件,所以无法加载具体函数的调用位置,只能手动去计算 具体步骤是:在 windbg.exe 中加载 se ...
- win32-StretchDIBits - PrintDlg
使用StretchDIBits将位图数据传输到printer的dc中 #include <Windows.h> #include <algorithm> int main() ...
- 深入理解Go语言(04):scheduler调度器-GMP里结构体源码分析
在前面一节中简单介绍了golang的调度模型-GPM模型,介绍了他们各自的作用.这篇文章就来看看他们的源码结构. Go版本:go1.13.9 M结构体 M结构体是OS线程的一个抽象,主要负责结合P运行 ...
- MyBaits查询MySQL日期类型结果相差8个小时
问题描述 在Java项目中使用MyBatis作为ORM框架,但是查询出的MySQL日期类型字段值总是比数据库表里的值多8个小时. 具体说明: MySQL数据库表字段类型为timestamp,映射的Ja ...