这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

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.jsglobal对象呀,也就是说在Node.js的全局作用域中this的指向将会是global对象

console.log(this === window); // true

二、函数上下文调用

2.1 函数直接调用

普通函数内部的this指向有两种情况:严格模式和非严格模式。

非严格模式下,this默认指向全局对象,如下:

function fn1() {
console.log(this);
}
fn1(); // window

严格模式下,thisundefined。如下:

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

callapplybind作用是改变函数执行时的上下文,也就是改变函数运行时的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);

可以看到,callthis的指向变成了obj,而直接调用fn()方法this的指向是window对象

如果把第一个参数改为nullundefined,那结果是怎样的呢?答案: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]);

可以看到,applythis的指向变成了obj,而直接调用fn()方法this的指向是window对象

同样,如果第一个参数为nullundefinedthis默认指向window

applycall唯一的不同就是第二个参数的传入形式: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 小结

从上面可以看到,applycallbind三者的区别在于:

  • 都可以改变函数的this对象指向;
  • 第一个参数都是this要指向的对象,如果没有这个参数或参数为undefinednull,则默认指向window对象
  • 都可以传参,apply是数组,call是参数列表,且applycall是一次性传入参数,而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,任何方法都改变不了其指向,如callbindapply

四、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 显式绑定

callapplybind对 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的相关原理知识就介绍完了,现在转过头再去看字节的那道面试题是不是就可以轻松作答了。thisjavascript中最基础的知识点,往往就是这些最基础的知识让我们在很多面试过程中一次次倒下,所以基础知识真的很有必要再去深入了解,这样不管是在以后的工作还是面试中都能够让我们自信十足,不再折戟沉沙。

本文转载于:

https://juejin.cn/post/7089690603755143199

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--一道字节面试题引出的this指向问题的更多相关文章

  1. 一道sql面试题(查询语句)

    一道sql面试题(查询语句)   id name age 1  a        11 2  b        11 3  c        12 4  d        13 5  e        ...

  2. 一道 JavaScript 面试题

    有一道 JavaScript 面试题. f = function () { return true; }; g = function () { return false; }; (function() ...

  3. 一道经典面试题-----setTimeout(function(){},0)

    一道经典面试题-----setTimeout(function(){},0) 转载: http://www.w3cfuns.com/notes/17398/e8a1ce8f863e8b5abb5300 ...

  4. 一道Python面试题

    无意间,看到这么一道Python面试题:以下代码将输出什么? def testFun():    temp = [lambda x : i*x for i in range(4)]    return ...

  5. new与属性访问的顺序,从一道JS面试题说起

    这段时间一直在研究设计模式,在看工厂模式的时候,看到一段代码 VehicleFactory.prototype.createVehicle = function ( options ) { if( o ...

  6. 记录一道神仙CTF-wtf.sh-150

    记录一道完全超出我能力的CTF神仙题(不愧是世界级比赛的真题orz),此题我仅解出了第一部分的flag,第二部分则参考了WP.不得不说这种题目解出来还是很有自豪感的嘛~  直接看题! 0x01 第一部 ...

  7. 从一道网易面试题浅谈 Tagged Pointer - darcy_tang 的博客

    前言 这篇博客九月就想写了,因为赶项目拖了到现在,抓住17年尾巴写吧~ 正文 上次看了一篇 <从一道网易面试题浅谈OC线程安全> 的博客,主要内容是: 作者去网易面试,面试官出了一道面试题 ...

  8. 字节跳动的一道python面试题

    #!/usr/bin/python #coding=utf-8 #好好学习,天天向上 lst = ['hongkong','xiantyk','chinat','guangdong','z'] lst ...

  9. why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。

    这是why的第 65 篇原创文章 荒腔走板 大家好,我是 why,欢迎来到我连续周更优质原创文章的第 65 篇.老规矩,先荒腔走板聊聊技术之外的东西. 上面这图是去年的成都马拉松赛道上,摄影师抓拍的我 ...

  10. 关于fork的一道经典面试题

    这是一道面试题,问程序最终输出几个“-”: #include<stdio.h> #include<sys/types.h> #include<unistd.h> i ...

随机推荐

  1. 文心一言 VS 讯飞星火 VS chatgpt (198)-- 算法导论14.3 6题

    六.用go语言,说明如何来维护一个支持操作MIN-GAP的一些数的动态集Q,使得该操作能给出Q中两个最接近的数之间的差值.例如,Q=(1,5,9,15,18,22),则MIN-GAP返回18-15=3 ...

  2. STC8H8K64U 的 USB 功能测试(续)

    对 STC8H8K64U 的USB测试昨天没搞定, 判断可能是供电的问题, 直接用5V不行, 从USB2TTL上采电3.3V时存在一个问题, 就是 D-/D+ 在上电前就已经连接了, 不符合 USB ...

  3. SpringCloud Ribbon负载均衡服务调用实战

    介绍 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具. 主要功能是提供客户端的软件负载均衡算法和服务调用.Ribbon客户端组件提供一系列完善 ...

  4. 深入理解Go语言(08):sync.WaitGroup源码分析

    一.sync.WaitGroup简介 1.1 sync.WaitGroup 解决了什么问题 在编程的时候,有时遇到一个大的任务,为了提高计算速度,会用到并发程序,把一个大的任务拆分成几个小的独立的任务 ...

  5. FileBeat简单使用

    简介 首先要了解ELK架构 这种结构因为需要在各个服务器上部署 Logstash,而它比较消耗 CPU 和内存资源,所以比较适合计算资源丰富的服务器,否则容易造成服务器性能下降,甚至可能导致无法正常工 ...

  6. expect tcl 摘录

    目录 部分参考来源说明 例子 expect命令 核心命令有三个 spawn.expect.send 其他expect命令 expect命令的选项 变量 tcl摘录 数据类型 符号 命令 其他说明 部分 ...

  7. 关于KMP模式匹配的一些思考

    算法简介 模式匹配 给定主串text和模式串pattern,在主串中查找,如果找到了模式串,返回模式串在主串中的起始位置,从1开始计数. 暴力求解求解模式匹配 算法的核心思想是:蛮力法.即使用两个指针 ...

  8. C++基本知识梳理

    一.命名空间 概念:命名空间是新定义的一个作用域,里面可以放函数,变量,定义类等,主要用来防止命名冲突. 实现:namespace关键字 命名空间名字{ 命名空间成员 } 注意点: 1.命名空间可以嵌 ...

  9. stm32OLED多级菜单

    今天实现了OLED多级菜单的显示.我用的是stm32f103ve,和四脚的OLED屏幕,用了三个按键. 话不多说,直接上代码. 点击查看代码 //先定义按键功能结构体 typedef struct { ...

  10. putty配置kali linux 远程连接

    首先配置sshd_config文件 VI 编辑或者使用 gedit 文本编辑, 修改的内容包括下面几个 红色标出(为了以复原建议大家 拷贝一份或者修改的地方进行标注) 之后重启服务,但是有的还是存在报 ...