jjzjj

记录--一道字节面试题引出的this指向问题

林恒 2023-03-28 原文

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

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. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  4. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  5. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  6. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  7. ruby-on-rails - Rails 5 Active Record 记录无效错误 - 2

    我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa

  8. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  9. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  10. Ruby - 如何将消息长度表示为 2 个二进制字节 - 2

    我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi

随机推荐