《你不知道的Javascrpit上卷》 this全面解析

96 阅读11分钟

背景

        关于this,掘金有很多博客说明,js中的this指向问题也是面试常考,受到之前导师的影响,最近有点空闲时间,又捡起专业书籍来看,本文根据《你不知道的Javascript上卷》第二章来完成,这里面对this的解析相当全面,推荐和我一样的根基不稳的同伴阅读。

为什么要使用this

我们看如下代码

function identify() {
   return this.name.toUpperCase();
}
function speak() {
   var greeting = "Hello, I'm " + identify.call(this);
   console.log(greeting);
}
var me = {
   name: "Kyle",
};
var you = {
   name: "Reader",
};
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是 KYLE
speak.call(you); // Hello, 我是 READER

        这段代码是可以在不同的上下文对象(me and you甚至更多)中重复使用identify和speak函数
如果我们不使用this我们则需要在函数中将我们的上下文对象以参数的形式传递

function identify(context) {
   return context.name.toUpperCase();
}
function speak(context) {
   var greeting = "Hello, I'm " + identify(context);
   console.log(greeting);
}
identify(you); // READER
speak(me); //hello, 我是 KYLE

        this 提供了一种更优雅的方式来 隐式“传递” 一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this则不会这样

对this的误解

        关于this笔者在看这本书之前的误解和书中完全相似,只不过凭借面试经验以及八股的buff加持下没有过多踩坑,但是让笔者给别人解释也说不出具体原由

this指向自身

        人们很容易把 this 理解成指向函数自身,为什么会有这种想法?既然函数看作一个对象(JavaScript 中的所有函数都是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有用。

function foo(num) {
    console.log("foo: " + num);
    // 记录 foo 被调用的次数
    this.count++; // 看到后面其实会了解到 这个this指向window
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
    if (i > 5) {
        foo(i);
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log(foo.count); // 0

        其实上面的代码 foo.count,count只不过是foo函数上面的属性。如果要记录调用次数,怎么办呢?在上面代码添加一个对象来记录 或者 foo.count++、或者显示绑定this(call apply bingd)后续会将这里不再赘述;

function foo(num) {
        console.log("foo: " + num);
        // 记录 foo 被调用的次数
        data.count++;
        // 或者
        foo.count++;
    }
var data = {
    count:0
}

这种方法只是巧妙地运用了作用域罢了

this指向作用域

        这种误解是比较复杂的,因为在某种情况下是正确的,但是在其他情况下他却是错误的,但是需要明确的是this 在任何情况下都不指向函数的词法作用域,虽然作用域和对象类似,但是作用域“对象”无法通过Javascript代码访问,他只存于JS引擎内部
思考一下下面的代码,它试图(但是没有成功)跨越边界,使用 this 来隐式引用函数的词法作用域:

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo(); // ReferenceError: a is not defined

        首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是在非浏览器环境绝对不可能成功的,之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西。每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

所以This到底是什么呢?

        this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

调用位置

        在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个问题:这个 this 到底引用的是什么?
接下来看一段代码来理解调用位置和声明位置

function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log("baz");
    bar(); // <-- bar 的调用位置
}
function bar() {
    // 当前调用栈是 baz -> bar
    // 因此,当前调用位置在 baz 中
    console.log("bar");
    foo(); // <-- foo 的调用位置
}
function foo() {
    // 当前调用栈是 baz -> bar -> foo
    // 因此,当前调用位置在 bar 中
    console.log("foo");
}
baz(); // <-- baz 的调用位置

this四种绑定规则

默认绑定

最常用函数调用类型:独立函数调用

function foo() {
    console.log( this.a );
}
var a = 2;
foo(); // 2

        在函数调用 a变量会挂在到全局window上面,而函数的独立调用 this是指向window的,但是也有特殊情况:严格模式

function foo() {
    'use strict';
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined

        在函数内部是严格模式下,不能将全局对象window适用于默认绑定,因此this指向的是undefined,但是!!!!!!!!!这里还有一个区分,我们是在定义的时候是用的严格模式,如果我们调用的时候是所处严格模式呢?

function foo() {
    console.log(this.a);
}
var a = 2;
(function () {
    'use strict';
    foo(); // 2
})();

可见在严格模式下调用函数不影响默认绑定。

隐试绑定

        另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导

function foo() {
    console.log(this.a);
}
let obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

        首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它。无论如何称呼这个模式,当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
对象属性引用链只有在上一层或者说最后一层在调用位置中起作用。 比如 obj1.obj2.obj3.obj4.foo() foo的this指向obj4的

对于隐式绑定有隐式丢失问题

被隐式绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把this绑定到全局对象或者undefined对象

var obj = {
    a: 2,
    foo: function foo() {
        console.log(this.a);
    }
};
var a = 5;
let fn = obj.foo;
fn();

上面的代码虽然fn是obj.foo的一个引用,其实但是实际上他引用的是foo函数本身

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
// fn 其实引用的是 foo
    fn(); // <-- 调用位置!
}
let obj = {
    a: 2,
    foo: foo
};
let a = 'oops, global'; // a 是全局对象的属性
doFoo(obj.foo); // "oops, global"

包括对于函数作为参数传递也是类似,参数传递其实是一种隐式赋值,这也是一种回调函数的形式

var obj = {
    a: 2,
    foo: function foo() {
        console.log(this.a);
    }
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

这种回调函数的this则隐式丢失了,比如setTimeout、setInterval这些是全局的 里面的this是指向window的,再比如一些事件回调:click、move、dragger等等回调函数里面的this则是指向事件的dom元素

显示绑定

        显示绑定就是我们常见的call apply bind,直接将函数的this绑定在某个对象上面 关于用法这里不再赘述可以提供一下如何实现,这个也是面试常考

Function.prototype.myApply = function (obj, arr) {
    obj.fn = this;
    obj.fn(...arr);
    delete obj.fn;
};
Function.prototype.mycall = function (obj, ...args) {
    obj.fn = this;
    obj.fn(...args);
    delete obj.fn;
};
Function.prototype.myBind = function (obj, ...args) {
    obj.fn = this;
    return () => {
        obj.fn(...args);
    };
};
let obj = {
    name: '张三'
};
function Person(age, address) {
    console.log(this.name, age, address);
}
Person.myApply(obj, ['18', '四川']);

硬绑定

        但是呢显示绑定也无法解决之前提出的绑定丢失问题,但是显示绑定的另外一个变种 硬绑定 可以解决这个问题

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
var bar = function () {
    foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call(window); // 2

我们创建了函数 bar(),并在它的内部手动调用了 foo.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
let obj = {
    a: 2
};
let bar = function () {
    return foo.apply(obj, arguments);
};
let b = bar(3); // 2 3

API调用上下文

        没看文章之前作者也犯了错误,之前用一些数组API 比如 forEach 就知道第一个参数,没关注第二个参数,其实数组很多API第二个参数都是需要传递回调函数的上下文 也就是this,只不过大部分场景用不到。

image.png image.png

new 绑定

在JS中 构造函数只是一些使用new操作符时被调用的函数,并不属于某个类,也不会实例化一个类 一些内置对象比如Number、Date都可以通过new来调用,这里有一个重要但是细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。 使用new来调用函数或者发生构造函数调用会自动执行下面操作

  1. 创建一个全新对象
  2. 将新对象的原型与构造函数的原型连接
  3. 新对象绑定到函数
  4. 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象
let myNew = (constructor, ...arg) => {
    let obj = {};
    obj.__proto__ = constructor.prototype;
    const result = constructor.apply(obj, arg);
    return result === 'object' && result !== null ? result : obj;
};
function Person(name, age) {
    this.name = name;
    this.age = age;
}

const person = myNew(Person, 'Alice', 30);
console.log(person);
console.log(person instanceof Person);

更改this的优先级

毫无疑问 默认绑定的优先级最低,其次是隐式绑定,那么显示绑定和new绑定那个优先级更高呢?之前说过硬绑定是显示绑定的变种,我们看看如下代码:

function foo(something) {
    this.a = something;
}
let obj1 = {};
let bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
let baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

         bar 被硬绑定到 obj1 上,但是 new bar(3) 并没有像我们预计的那样把 obj1.a修改为 3。相反,new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。因为使用了new 绑定,我们得到了一个名字为 baz 的新对象,并且 baz.a 的值是 3。 可以看出new 绑定优先级高于显示绑定
结论 new > 显示绑定 > 隐式绑定 > 默认绑定

绑定的例外

被忽略的绑定

当我们用Null 或者undefined作为this的绑定对象传入call、apply、bind 这些值调在调用会被忽略,实际应用的是默认绑定规则: 函数独立调用

this词法

这里this词法指的是this是指向的作用域,这也是我们之前对this误解,之所以我们有这种误解是因为箭头函数以及隐式绑定的影响 对于箭头函数的this我们记住一句话:who定义箭头函数,那么箭头函数的this就属于who所处的上下文 也就是常说的箭头函数找他爹的上下午

function foo() {
// 返回一个箭头函数
    return (a) => {
        // this 继承自 foo()
        console.log(this.a);
    };
}
let obj1 = {
    a: 2
};
let obj2 = {
    a: 3

};
let bar = foo.call(obj1);
bar.call(obj2); // 2, 不是 3 !
// 同样的
function foo() {
    setTimeout(() => {
        // 这里的 this 在此法上继承自 foo()
        console.log(this.a);
    }, 100);
}
let obj = {
    a: 2
};
foo.call(obj); // 2

当然上述代码中setTimeout是箭头函数 如果是一个普通函数,this指向window,怎么在普通函数内使用外部的this呢?
答案:用一个变量保存一下this喽

function foo() {
    that = this;
    setTimeout(() => {
        console.log(that.a);
    }, 100);
}

最后 光说不练假把式

var length = 10;

function fn() {
  return this.length + 1;
}

var obj1 = {
  length: 5,

  test1: function () {
    return fn();
  },
};

const a = obj1.test1.call();
console.log("a:" + a);

const b = obj1.test1();
console.log("b:" + b);

obj1.test2 = fn;
const c = obj1.test2.call();
console.log("c:" + c);

const d = obj1.test2();
console.log("d:" + d);
var name = "123";
let obj = {
    name: "345",
    getName: function() {
        function getInfo() {
            console.log(this.name);
        }
        getInfo();
    }
};
// obj.getName();
var name = "123";
let object = {
    name: "345",
    getName: function() {
       let getInfo=()=> {
            console.log(this.name);
        }
        getInfo();
    }
};
// object.getName();

总结

这篇文章是根据《你不知道的javascript上卷》完成,可能平时看这些文章有助于面试啥的,但是基础总归要自己打牢,书籍的知识比较系统一点,而且看看书也就发现面试官考的就是书上的。最后

image.png