前端性能-首次加载优化70%

news2025/7/13 7:48:53

前言

本篇文章,我们来总结归纳下万恶的this以及衍生出来的call/apply/bind对this进行绑定,想了很久,决定用实例演示的方式来讲解this,这样才能够理解this,因为this确实变化莫测,只靠概念,是不能够理解的;之后如果有看到更好的案例,也会同步更新到文章中。

this的绑定方式

首先我们要牢记于心this的指向,是在函数被调用的时候确定的,因此,this的指向便非常灵活,情况多样,常见的this一共有5种绑定方式:

  • 默认绑定(非严格模式下this指向全局对象, 严格模式下this会绑定到undefined)
  • 隐式绑定(当函数(fn)引用有上下文对象的时候, 如 obj.fn()的调用方式, fn内部的this是指向obj)
  • 显式绑定(通过call()或者apply()方法直接指定this的绑定对象)
  • new操作符(执行过程中会将新生成的对象绑定到函数调用的this)
  • 箭头函数绑定(this的指向由外层作用域环境决定的)

那让我们开始找到相应的题目,进行分析。

1.默认绑定

我们先来看默认绑定: 非严格模式下,this指向window,严格模式下this指向undefined,这句话其实有些歧义和令人不解的地方,我们通过几个题目来分析一下。

题目一:

var a = 1;
function fn () {console.log(this.a)
}
fn(); 

我们知道,如果用var来声明变量(不在函数内)的话,会自动挂载到全局,全局调用函数,相当于是window调用了这个函数,所以上边这段代码相当于:

window.a = 1
function fn () {console.log(this.a)
}
window.fn() 

答案显而易见,输出为:

1 

而如果我们把var声明改为letconst,那么结果是什么呢?可以试一下,没错,是undefined,因为letconst声明的变量,不会被挂在到window对象中。

题目二:

"use strict";
var a = 1;
function fn () {console.log('inner-this', this)console.log(window.a)console.log(this.a)
}
console.log(window.fn)
console.log('outer-this', this)
fn(); 

当我们在最上边写上use strict的时候,相当于开启了严格模式,但所谓的严格模式,只是将fn函数中的this指向了undefined,二并不会影响到全局的this指向,所以inner-this打印的是undefinedouter-this打印的是window对象,全局下使用var声明的变量a,依旧会被挂在到全局。

所以输出结果为:

2.隐式绑定

这种就是面试中经常出现的类型,那么对于判断这种this的指向,我们只需要记住哪个对象最后调用函数,函数中的this就指向那个对象(箭头函数除外)。 我们来看题目:

题目一:

function fn () {console.log(this.a)
}
var obj = { a: 1, fn }
var a = 2
obj.fn() 

我们利用上边的那句口诀,经过分析,发现最后调用fn的,是obj对象,所以,fn函数内部的this,便指向obj对象,答案显而易见输出:

1 

还有两种隐式绑定的情况,非常具有迷惑性,很容易绕晕,那就是将函数赋给一个新的变量,或者将函数作为参数,进行传递,我们还是通过几道题目来分析。

题目一:

function fn () {console.log(this.a)
};
var obj = { a: 1, fn };
var a = 2;
var fn2 = obj.fn;
var obj2 = { a: 3, fn2 }

obj.fn();
fn2();
obj2.fn2(); 

先来判断obj.fn()的输出,没错,和上一题一样,很容易就能分析出此时this指向obj,输出1;而fn2被赋值成了obj.fn,在调用的时候,出现了隐式丢失,依旧是window来调用的,所以此时this指向window,输出2;再来分析obj2.fn2(),此时fn2是被obj2所调用了,又出现了隐式丢失,所以this指向obj2,输出3,最终结果为:

1
2
3 

题目二:

function foo () {console.log(this.a)
}
function fooWrapper (fn) {console.log(this)fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, fooWrapper }

obj2.fooWrapper(obj.foo) 

我们来分析一下,这道题中,fooWrapper函数是被obj2对象所调用的,所以我们可以先得出fooWrapper中的this是指向obj2对象的,所以先打印出{ a: 3, fooWrapper: f }其实就是obj2这个对象;而obj.foo被当做参数传递到了fooWrapper中,此时是window调用的fn,所以,此时会输出2,最终结果为:

{ a: 3, fooWrapper: f } // 其实就是obj2这个对象
2 

所以我们可以得出结论在函数被当成参数传递进另一个函数时,会发生隐式丢失的问题,其this并不会受外层包裹它的函数所影响,非严格模式下指向window,严格模式下指向undefined,现在再看这句话,是不是比最开始看到,明白了很多了呢?同样的代码,可以在顶部加上use strict查看结果,来验证这句话。

3.显示绑定

就是使用一些方法,强行绑定函数内部this的指向,比如call apply bind,需要先注意下他们之间的区别:

  • 经由call()apply()绑定的函数,会直接调用执行;
  • call()apply()用法几乎相同,第一个参数都为绑定的对象,如果第一个参数为null或者undefined的话,会自动忽略这个参数;他们之间的区别就在于之后的传参方式:call接收多个参数,apply接收一个数组;
  • bind()绑定会生成一个新的函数,需要手动再次调用,才会执行;

我们先通过一个比较简单的题目来看下他们之间的区别,顺便温习一下call apply bind的基本用法:

题目一:

function fn (x, y) {console.log(this.a)console.log(x + y)
}
var obj = { a: 1 }
var a = 2

fn(5, 6)
fn.call(obj, 5, 6)
fn.apply(obj, [5, 6])
fn.bind(obj)(5, 6) 

首先fn被调用的时候,因为是window调用的fn,所以此时fn中的this指向window,从而打印1, 11;接下来的3种显示绑定,fn中的this便指向了obj,然后立即调用执行(bind因为返回的是一个函数,所以还需要手动加括号进行调用),结果都输出1,11;我们可以发现区别如上所述,就是传参方式不同。

题目二:

var obj1 = {a: 1
}
var obj2 = {a: 2,fn1: function () {console.log(this.a)},fn2: function () {setTimeout(function () {console.log(this)console.log(this.a)}.call(obj1), 0)}
}
var a = 3
obj2.fn1()
obj2.fn2() 

首先可以分析出obj2.fn1中,因为是ob2调用的fn1,所以fn1中的this指向obj2,首先输出2;再来看obj2.fn2,因为函数作为setTimeout的参数传入发生了隐式丢失,所以函数内部的this按理来说应该是指向window的,但是我们使用了call方法,改变了this指向为obj1,所以输出{ a: 1 } 1,最后输出结果为:

2
{ a: 1 } // 就是obj1对象自己
1 

题目三:

再来看一道返回匿名函数发生隐式丢失,与显示绑定结合的题目吧。

var obj = {a: 'obj',fn: function () {console.log('fn:', this.a)return function () {console.log('inner-a:', this.a)}}
}
var a = 'outer-a'
var obj2 = { a: 'obj2' }

obj.fn()()
obj.fn.call(obj2)()
obj.fn().call(obj2) 

看起来花里胡哨,我们只要仔细阅读,就不难分析出结果:

  • 首先看obj.fn()(),注意了,为啥是两个括号呢?其实是执行了2个操作,首先执行了obj.fn()函数,输出结果为fn: obj,之后,obj.fn()返回了一个匿名函数后,又执行了这个匿名函数,因为是window调用的,所以此时又会输出inner-a: outer-a
  • 之后再来看obj.fn.call(obj2)(),比较长,我们慢慢来看,首先obj.fn注意此时没加括号,说明obj.fn没有被调用,可以理解为找到了这个fn,之后使用call方法,给obj.fn进行显示绑定到obj2对象,所以,此时obj.fn中的this是指向obj2的,首先输出了fn: obj2,之后,又出现了一个括号,和上边一样,也是匿名函数此刻被调用了,依旧是window对齐进行的调用,所以此时又会输出inner-a: outer-a
  • 最后再来看obj.fn().call(obj2),会发现好理解了许多,区别也很明显,call是为obj.fn()执行后,返回的匿名函数,进行了显示绑定,所以输出结果为fn: obj,之后又会输出inner-a: obj2

4.new操作符进行绑定

在普通函数被当成构造函数,执行new操作生成对象时,函数中的this会被绑定为新创建的对象。

题目一:

function Person (age) {this.age = agethis.fn1 = function () {console.log(this.age)}this.fn2 = function () {return function () {console.log(this.age)}}
}
var person1 = new Person(20)
person1.fn1()
person1.fn2()() 

我们可以分析到person1对象创建后person1.fn1()打印出来的就是构造函数中传入的20,所以会先输出20;而person1.fn2()返回的是一个匿名函数,之后匿名函数又被调用,调用者是window,而window.age没有被定义,所以会输出undefined

其实和用字面量创建对象十分类似,使用new关键字创建对象的this指向几乎是没有区别,可以靠同一套逻辑来进行判断。

5.箭头函数绑定

之前我们说过,哪个对象最后调用函数,函数中的this就指向那个对象(箭头函数除外),为啥箭头函数除外呢?因为箭头函数中的this,是要根据作用域链进行查找,来决定的。

题目一:

var obj = {name: 'obj',fn1: () => {console.log(this.name)},fn2: function () {console.log(this.name)return () => {console.log(this.name)}}
}
var name = 'window'
obj.fn1()
obj.fn2()() 

先来分析obj.fn1(),因为fn1是一个箭头函数,所以在调用的时候,其外部作用域是window,所以先输出window;再看obj.fn2()(),因为obj.fn2是个普通函数,所以执行时,先打印的this.nameobj,之后返回了一个匿名箭头函数,它的this指向是由外部作用域确定的,所以它内部的this其实用的就是fn2中的this,所以打印的this.name依旧是obj,最终输出为:

'window'
'obj'
'obj' 

题目二:

如果将普通函数和箭头函数嵌套,那么this该如何判断呢?根据排列组合我们可以得到4种情况:普通套普通;普通套箭头;箭头套普通;箭头套箭头,你别说,写项目的时候,还真会有人这样写,然后就会遇到很奇怪的bug。

var name = 'window'
var obj1 = {name: 'obj1',fn: function () {console.log(this.name)return function () {console.log(this.name)}}
}
var obj2 = {name: 'obj2',fn: function () {console.log(this.name)return () => {console.log(this.name)}}
}
var obj3 = {name: 'obj3',fn: () => {console.log(this.name)return function () {console.log(this.name)}}
}
var obj4 = {name: 'obj4',fn: () => {console.log(this.name)return () => {console.log(this.name)}}
}

obj1.fn()()
obj2.fn()()
obj3.fn()()
obj4.fn()() 

我们一条一条来分析:

  • obj1.fn()在执行后,因为fn是一个普通函数,所以内部的this指向obj,首先输出obj1,之后返回了一个匿名的普通函数,和前文说过的一样,是window来调用的,所以又会输出window,我们已经很熟悉了;
  • obj2.fn()在执行后,结果是obj2,因为返回的匿名函数是箭头函数,所以其内部的this使用的就是fn中的this,输出obj2
  • obj3.fn()是一个箭头函数,由外部作用域决定this取值,所以先输出window,返回的匿名普通函数,调用者是window,所以输出window
  • obj4.fn()()同理,是两个箭头函数,所以输出结果为两个window

我们还可以得出一个结论,箭头函数中的this由外层作用域决定,并且指向函数定义时的this,而并非调用时。同时,我们虽然没法用call apply bind来改变箭头函数中的this指向,但是我们可以通过改变箭头函数外层作用域的this指向,间接的改变箭头函数中this的指向。

关于this的内容,我们暂时告一段落,接下来,我们写一下之前使用过new call apply bind的原理,面试中的高频考点哦~

new的实现原理

//写一个模拟new的函数
function mockNew() {//获取第一个参数即构造函数,因为shift的返回值就是第一个参数,同时arguments数组中第一个参数被移除掉了let Constructor = [].shift.call(arguments);//创建一个新对象let obj = {};//新对象的__proto__指向构造函数的prototype,从而obj能方位原型上的属性obj.__proto__ = Constructor.prototype;// 上边两个步骤可以合并为let obj = Object.create(Construcrot.prototype),知识为了看的更清楚才分开写了//执行构造函数,改变this指向,使得obj能访问到构造函数中的属性let result = Constructor.apply(obj, arguments);// 加上这一步的作用就是如果构造函数有返回值(一般我们不会这样去做),那么就返回,否则,默认返回objreturn result instanceof Object ? result : obj;
}

// 试验一下
function Animal(type) {this.type = type;
}
Animal.prototype.say = function() {console.log('say');
}
let o = mockNew(Animal, '哺乳类');

o.say();
console.log(o.type); 

顺便提一句,既然我们用到了instanceof,那我们写一下其原理是怎么实现的吧~其实就是根据__proto__属性,在原型链上不断查找,instanceof左边是实例对象,右边是构造函数或类:

function instanceOf (A, B) {B = B.prototypeA = A.__proto__while (true) {if (A === null) {return false}if (A === B) {return true}A = A.__proto__}
}
// 尝试一下
class A {

}
let a = new A()
let res = instanceOf(a, A)
console.log(res) 

call实现原理

Function.prototype.call = function (context, ...args) {// 如果context为真值,那么将其包装成一个对象context = context ? Object(context) : window// 创建一个独一无二的fn名let fn = Symbol()// 将this赋值给context.fn属性,这里的this就是指调用call的那个函数context[fn] = this// 这样,在调用这个函数的时候,因为被放置在了context里边,所以this就会指向contextcontext[fn](...args)delete context[fn]return result
} 

apply实现原理

applycall是类似的,只不过传参不一样,能看懂call的话,那么apply也不在话下!

Function.prototype.apply = function(context, arr) {context = context ? Object(context) : window;let fn = Symbol();context[fn] = this;let result = arr ? context[fn](...arr) : context[fn]();delete context[fn];return result;
}; 

bind实现原理

bind在内部使用了apply,返回一个新的函数,所以代码如下:

Function.prototype.bind = function (context, ...args) {return (...argument) => {return this.apply(context, [...args, ...argument])}
}
// 尝试一下
let obj = {name: 'jw'
}
function fn (a, b) {console.log(this.name, a + b)
}
let bindFn = fn.bind(obj, 9)
bindFn(3) 

结尾

关于this的问题,我们看完这篇文章,大致就能比较清楚的判断了,如果在项目中,因为this指向出现一些问题,也能及时的排查出来。下次看到了绕来绕去代码,this乱指,如果是在面试中,不妨耐着性子一点点梳理,如果是你同事写出来的代码,一巴掌直接呼过去了就。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/5603.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JS 构造|原型|原型链|继承(圣杯模式)|ES6类语法】下篇

⌚️⌚️⌚️个人格言:时间是亳不留情的,它真使人在自己制造的镜子里照见自己的真相! 📖Git专栏:📑Git篇🔥🔥🔥 📖JavaScript专栏:📑js实用技巧篇…

【数据结构】带头双向循环链表基本操作的实现(C语言)

🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。 🐌 个人主页:蜗牛牛啊 🔥 系列专栏:🛹初出茅庐C语言、🛴数据结构 📕 学习格言:博…

峰会实录 | StarRocks PMC Chair 赵纯:数据分析的极速统一3.0 时代

作者:StarRocks PMC Chair 赵纯(本文为作者在 StarRocks Summit Asia 2022 上的分享) 一年前,StarRocks 源码开放,StarRocks 社区也正式成立。经过一年发展,社区已经获得了 3400 个 Star,7500 …

如何用windows上架ios到苹果商城

1.苹果账号 1.你需要申请苹果账号 官网有提示:Sign In - Apple 2.登录 登录后店家 account,进入account。 点击证书,进入。 2.开始上架步骤 1.注册标识符(Bundle ID) 进入这个界面后,点击 Identifiers …

Elasticsearch快照备份

目录 1、Repositories 1、配置路径 2、注册快照存储库 2、查看注册的库 3、创建快照 1、为全部索引创建快照 2、为指定索引创建快照 4、查看备份完成的列表 5、删除快照 6、从快照恢复 1、恢复指定索引 2、恢复所有索引(除.开头的系统索引) …

【Redis】 数据结构:Redis对象与编码(底层结构)对应关系详解

【Redis】 数据结构:Redis对象与编码(底层结构)对应关系详解 文章目录【Redis】 数据结构:Redis对象与编码(底层结构)对应关系详解Redis对象与编码(底层结构)对应关系引入Redis数据结构-RedisObjectredisObject数据结构Redis的编码方式五种数据结构Redis…

2022年深信服杯四川省大学生信息安全技术大赛-CTF-Reverse复现(部分)

Rush B 开始先设置一下数字以16进制格式显示 看主函数 __int64 __fastcall main(int a1, char **a2, char **a3) {int v3; // eaxsize_t v4; // raxint v5; // ecxchar v6; // alint v7; // ecxint v9; // [rsp3Ch] [rbp-404h]char s[1000]; // [rsp40h] [rbp-400h] BYREFchar …

免杀技术(详细)

恶意软件 ● 病毒、木马、蠕虫、键盘记录、僵尸程序、流氓软件、勒索软件、广告程序 ● 在用户非资源的情况下执行安装 ● 出于某种恶意的目的:控制、窃取、勒索、偷窥、推送、攻击。。。。。 恶意程序最重要的防护手段 ● 杀毒软件 / 防病毒软件 ● 客户端 / 服…

c# .net MAUI基础篇 环境安装、新建项目、安卓模拟器安装、项目运行

c# .net MAUI基础篇 环境安装、新建项目、安卓模拟器安装、项目运行 免费教学视频地址由趣编程ACE老师提供: 1..NET MAUI优势及安装和创建_哔哩哔哩_bilibili 一、介绍 .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移…

【面经】之小鼠喝药问题

题目 现在有 10 只小白鼠和 1000 支药水,1000 支药水中有且仅有一支药水有毒,如果小白鼠喝下毒药,那么毒发的时间是两小时。 现在只给你两小时的时间,请问如何用这 10 只小白鼠测出哪支药水有毒?(忽略小白…

【Java编程进阶】标识符和关键字

在学习Java程序设计基础的时候,主要有标识符,变量,数据类型,流程控制这些主要的内容。 推荐学习专栏:Java 编程进阶之路【从入门到精通】 文章目录1. 标识符2. 关键字1. 标识符 什么是标识符? 标识符是用…

linux下的PPPOE设置

1.打开终端 #sudo pppoeconf 进入配置,输入用户名和密码. 2.建立连接 #sudo pon dsl-provider 3.断开连接 #sudo poff dsl-provider Welcome to the ADSL client setup. First, I will run some checks on your system to make sure the PPPoE client is installed properly.…

The 2022 CCPC Guangzhou Onsite M. XOR Sum(数位dp 数位背包)

题目 给定n,m,k(0<n<1e15,0<m<1e12,1<k<18)&#xff0c; 求长度为k的数组a&#xff0c;ai为[0,m]的整数&#xff0c; 满足的方案数 答案对1e97取模 题解 第一反应想起了hdu3693&#xff0c;但比对了一下&#xff0c;感觉那个题难很多&#xff0c; 两年…

一看就会的Java方法

文章目录一、方法的定义和使用&#x1f351;1、为什么引入方法&#xff1f;&#x1f351;2、方法的定义&#x1f351;3、方法调用的执行过程&#x1f351;4、实参和形参的关系二、方法重载&#x1f351;1、为什么需要方法重载&#x1f351;2、方法重载的概念和特点&#x1f351…

四旋翼无人机学习第8节--OpenMV电路分析

这里写目录标题0 前言1 openmv优秀作品介绍2 stm32单片机原理图绘制3 stm32单片机外接电容分析3 stm32单片机外接电容绘制4 stm32单片机外接晶振分析5 stm32单片机外接晶振绘制6 stm32单片机复位电路分析7 stm32单片机复位电路设计0 前言 简单的说一下&#xff0c;openmv模块是…

微信小程序 | 吐血整理的日历及日程时间管理

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;小程序开发成神之路 --【这是一个为想要入门和进阶小程序开发专门开启的精品专栏&#xff01;从个人到商业的全套开发教程&#xff0c;实打实的干货分享&#xff0c;确定不来看看&#xff1f; …

关于宝宝过敏原检测的这几点,专家达成共识啦

随着传染病发病率的下降&#xff0c;儿童过敏性疾病的发病率逐年上升&#xff0c;引起了公众和医务人员的广泛关注。四川省妇幼保健医院检验科目前可进行过敏原检测。根据超敏反应的发生机制和临床特点&#xff0c;可分为四种类型。我们所谓的过敏原检查是特异性的IgE相关的Ⅰ超…

React源码之Fiber架构

对于Fiber我们可以理解为存储在内存中的Dom 对于React15在render阶段的reconcile是不可打断的&#xff0c;如果在操作大量的dom时&#xff0c;会存在卡顿&#xff0c;因为浏览器将所有的时间都交给了js引擎线程去执行&#xff0c;此时GUI渲染线程被阻塞&#xff0c;导致页面出现…

PyTorch搭建循环神经网络(RNN)进行文本分类、预测及损失分析(对不同国家的语言单词和姓氏进行分类,附源码和数据集)

需要源码和数据集请点赞关注收藏后评论区留言~~~ 下面我们将使用循环神经网络训练来自18种起源于不同语言的数千种姓氏&#xff0c;并根据拼写方式预测名称的来源。 一、数据准备和预处理 总共有18个txt文件&#xff0c;并且对它们进行预处理&#xff0c;输出如下 部分预处理…

Windows版Ros环境的搭建以及Rviz显示激光点云信息

安装步骤&#xff1a; 1.安装visual studio 2019-2022 2.安装ROS 3.创建ROS快捷终端 4.运行测试效果 一、安装Visual Studio 2022 需要利用vs编译ROS代码&#xff0c;所以需要安装Visual Studio 2022 这里注意要使用vs2022&#xff0c;ROS wiki给的教程是使用2019 1).使…