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

前言

本篇文章,我们来总结归纳下万恶的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,依旧会被挂在到全局。

所以输出结果为:

前端性能-首次加载优化70%_第1张图片

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道面试题,并对每个问题作出了回答和解析。

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

部分文档展示:



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

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

你可能感兴趣的:(前端,javascript,开发语言)