前语

  • 在 深入理解V8履行流程、履行上下文和效果域! 中讲到,JavaScript每履行一段可履行代码时,都会创立相关于的履行上下文,关于每个履行上下文,都包含三个重要的特点:
  1. 词法环境(LexicalEnvironment) 组件;
  2. 变量环境(VariableEnvironment)组件;
  3. 初始化 this 的值;

假如对效果域和履行上下文不太了解的同学能够看一下上面的提到的文章,这儿讲述了 V8 的编译过程,以及效果域和履行上下文等令人难懂的概念,相信你阅读完会有很大的收获!

什么是this

  • 与其他语言比较,函数的this关键字在 JavaScript 中的表现略有不同,此外,在严厉形式和非严厉形式之间也会有一些差别。在大局上下文中,无论是严厉形式或许非严厉形式,this都指向顶层目标(浏览器中是window)。
  • 在绝大多数情况下,函数的调用办法决议了this的值(运行时绑定)。this不能在履行期间被赋值,而且在每次函数被调用时this的值也可能会不同。
  • this是在运行时绑定的的,并不是在编写时绑定的,它的履行上下文取决于函数调用时的各种条件。
  • this的绑定和函数声明的方位没有任何关系,只取决于函数的调用办法。

绑定规矩

默许绑定

  • 首要要介绍的是最常用的函数调用类型: 独立函数调用,考虑以下代码:
function f00() {
  console.log(this.a);
}
var a = 2;
foo(); // 2
  • 在最初提到的那篇文章中说过,在大局效果域下用 var 关键字声明的变量和在大局声明的 函数 会被挂载到大局目标(window)上。
  • 当咱们看到调用 foo() 时,咱们都知道,大局声明的函数的效果域是顶层的 globalObject 在浏览器中也便是 window
  • 经过调查,咱们能够看出,在代码中,foo() 是直接运用不带任何润饰的函数引证进行调用的,因而只能运用默许绑定,所以函数中的 thiswindow,也便是 window.a,所以自可是然的就输出 2 了。
  • 假如运用严厉形式 strict mode ,则不会将大局目标用于默许绑定,由于 this 会绑定到 undefined;
function f00() {
  "use strict";
  console.log(this.a);
}
var a = 2;
f00(); // Cannot read properties of undefined (reading 'a')
// 由于严厉默许情况下,默许绑定,this会被绑定为 undefined ,所以this.a也就等于undivided.a
// 由于 undefined 下没有 a 的特点,所以会报类型过错
  • 值得注意的是,假如 foo()运行在非 strict mode 下时,默许绑定才干绑定到大局目标,在严厉形式 foo() 则不影响默许绑定。
function f00() {
  console.log(this.a);
}
var a = 2;
(function () {
  "use strict";
  f00(); // 2
})();

隐式绑定

  • 隐式绑定的规矩是调用方位是否有上下文目标,或许说是否被某个目标具有或许包含,可是这样的说法可能不太会,先来考虑下面的代码:
function foo() {
  console.log(this.a);
}
var obj = {
  a: 111,
  foo,
};
obj.foo(); // 111
  • 首要需求注意的是 foo() 的声明办法,以其之后是如何被当做引证特点添加到 obj 目标中的。可是无论是直接在 obj 中界说仍是先界说再添加为引证特点,这个函数严厉来说都不属于 obj 目标。
  • 可是调用方位会运用 obj 上下文来引证函数,因而你能够说函数被调用时 obj 目标 “具有” 或许 “包含” 函数引证。
  • 当函数引证有上下文目标时,隐式绑定规矩会把函数调用中的this绑定到这个上下文目标。因而 this.aobj.a 是相同的。

你小子,又在偷偷学this指向

  • 目标特点引证链只要上一层或许说终究一层在调用方位中起效果,例如
function foo() {
  console.log(this.a);
}
var obj2 = {
  a: 111,
  foo,
};
var obj1 = {
  a: 777,
  obj2,
};
obj1.obj2.foo(); // 111
// 目标 obj2 为终究一层
// obj1.obj2 仅为特点查找,并还没有开端调用

函数脱离原上下文

  • 一个最常见 this 绑定问题便是被隐式绑定的函数会丢失绑定目标,也便是说他会运用默许绑定默许。
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo,
};
var bar = obj.foo; // 函数别号
var a = "我是window下的a";
bar(); // 我是window下的a
  • 虽然 barobj.foo 的一个引证,可是实际上,它引证的是 foo 函数的本身,因而此时的 bar() 其实是一个一般的函数调用 因而运用了默许绑定。
  • 这实际上是从头界说了一个 bar 函数,和目标的结构相同,都是从头赋值,参考一下代码:
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo,
};
var { foo } = obj; // 这儿相当于从头界说了一个函数或许说这是一个函数别号
var a = "我是window下的a";
foo(); // 我是window下的a
var object = {
  moment: 777,
  age: 18,
};
console.log(object); // {moment: 777, age: 18}
var { moment } = object;
moment = "牛逼";
console.log(moment); // 牛逼
console.log(object); // {moment: 777, age: 18}
  • 上面的代码,解构出来的变量 moment,实际上在大局效果域中创立了一个变量 moment 并赋值为 777,后面的直接修改变量不修改目标 object 中的特点 moment

你小子,又在偷偷学this指向

函数作为参数

function foo() {
  console.log(this.a);
}
function bar(fn) {
  // fn 其实是引证 foo
  fn();
}
var obj = {
  a: 777,
  foo,
};
var a = "牛逼啊,这也行";
bar(obj.foo); // 牛逼啊,这也行
  • 参数传递其实便是一种隐式赋值,因而咱们传入函数时也会被隐式赋值,上面这段代码实际上便是以下代码的变体:
function foo() {
  console.log(this.a);
}
function bar() {
  const fn = obj.foo;
  fn();
}
var obj = {
  a: 777,
  foo,
};
var a = "牛逼啊,这也行";
bar(); // 牛逼啊,这也行

显现绑定

  • JavaScript 中,无论是宿主环境供给的一些函数仍是你自己创立的函数,你都能够运用 call(...)apply(...) 办法。
  • 他们的第一个参数是一个目标,是给this预备的,接着在调用函数时将其绑定到 this。由于你能够直接指定 this 的绑定目标,因而咱们称之为 显现绑定
  • 这儿 apply 和 call的语法规矩就不讲了,有需求的能够去 mdn 官网查阅。

硬绑定

  • 硬绑定 这种办法能够把 this 强制绑定到指定的目标 (new 在外),既然有 硬绑定 ,自然也有 软绑定 ,在后文中咱们会讲到。
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
};
var bar = function () {
  foo.call(obj);
};
bar(); // 2
setTimeout(bar, 1000); // 2
// 硬绑定的 bar 不可能再修改他的 this
bar.call(window); // 2
  • apply 办法也相同的成果,只不过参数参数的办法不相同。
  • bind 办法会回来一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文调用原始参数。

API调用的 “上下文”

  • JavaScript 语言和 宿主环境 供给了许多内置函数,都供给了一个可选的参数,通常成为 上下文,其效果和 bind(...) 相同,确保你的回调函数运用指定的 this

function callback(element) {
  console.log(element, this.id);
}
var obj = {
  id: "真不错",
};
// 调用 foo(...) 时把 this 绑定到 obj 上
[1, 2, 3].forEach(callback, obj);
// 1 '真不错'  2 '真不错'  3 '真不错'
// 俺 map 也相同
[1, 2, 3].map(callback, obj);
// 1 '真不错'  2 '真不错'  3 '真不错'

new绑定

  • 在开端讲绑定之前,我想你现已知道了运用 new 来调用结构函数会履行什么操作,咱们就再回顾一下吧:
  1. 在内存中创立一个新目标;
  2. 这个新目标内部的 [[prototype]] 特性 被赋值为结构函数的 prototype特点 (假如不了解这个也能够 点击这儿);
  3. 结构函数中内部的 this 被赋值为这个新目标(即 this 指向新目标);
  4. 履行结构函数内部的代码(给新目标添加特点);
  5. 假如结构函数回来非空目标,则回来该目标;否则,回来刚创立的新目标;

function Foo(moment) {
  this.moment = moment;
}
var bar = new Foo(777);
console.log(bar.a); // 777
  • 运用 new 来调用 Foo(...) 时,咱们会结构一个新目标并把他绑定到 Foo(...) 调用中的 this 上。
  • 咱们再来考虑一下的代码输出成果是什么:
var mayDay = {
  moment: "moment",
};
function Foo() {
  this.moment = 777;
  return mayDay;
}
var bar = new Foo();
console.log(bar.moment);
  • 终究输出的成果是 moment,也便是 this 被绑定到了 mayDay 目标上,那么为什么会这样呢?

答案就在 new 的终究一条过程 “假如结构函数回来非空目标,则回来该目标;否则,回来刚创立的新目标” 这条规矩上。

  • 换句话说便是,假如结构函数回来一个目标,则该目标将作为整个表达式的值回来,而传入的结构函数的 this 将会被抛弃。
  • 假如结构函数回来的是非目标类型,则疏忽回来值,回来新创立的目标
var mayDay = {
  moment: "moment",
};
function Foo() {
  this.moment = 777;
  return 111; // 这儿的回来值变化了
}
var bar = new Foo();
console.log(bar.moment); // 777 输出的是新目标的 moment

类上下文

  • this 在 类中的表现与函数中类似,由于类本质上也是函数,但也有一些区别和注意事项。在类的结构函数中,this 是一个惯例目标。类中所有非静态的办法都会被添加到 this 的原型中:

class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first() {}
  second() {}
  static third() {} // 这儿不在 this 上,在类本身上
}
new Example(); // ['constructor', 'first', 'second']

箭头函数调用

箭头函数表达式的语法比函数表达式更简洁,而且没有自己的 this, arguments,supernew.target。箭头函数表达式更适用于那些原本需求匿名函数的当地,而且它不能用作结构函数。正是由于箭头函数没有 this,自可是然的就不能运用 new 操作符了。


var moment = "moment";
var bar = {
  moment: 777,
  general: function () {
    console.log(this.moment);
  },
  arrow: () => {
    console.log(this.moment);
  },
  nest: function () {
    var callback = () => {
      console.log(this.moment);
    };
    callback();
  },
};
bar.general(); // 777
bar.arrow(); // moment
bar.nest(); // 777
  • 其间第一个一般函数的便是咱们前面说的隐式绑定。
  • 第二个调用由于箭头函数没有自己的 this ,他会查找箭头函数上一层的的一般函数的 this,这时演变成了默许绑定了,是大局调用。
  • 第三个和第二个类似,可是它查找的上一层是函数 nest ,这是一个隐式绑定了,自然也就输出目标内部的 monent
  • 虽然箭头函数无法经过 call, applu , bind 绑定 this ,可是他能够绑定缓存箭头函数上层的一般函数的 this,例如:

var foo = {
  moment: 777,
  general: function () {
    console.log(this.moment);
    return () => {
      console.log("arrow:", this.moment);
    };
  },
};
var obj = {
  moment: "moment",
};
foo.general().call(obj); // 777  "arrow: 777 "
foo.general.call(obj)(); // 'moment' 'arrow:' 'moment'
  • 注意 settimeout自履行函数 中的 this 指向 window

setTimeout(function foo() {
  console.log(this); // window
}, 0);
(function () {
  console.log(this); // window
})();
  • 由于 settimeout这个办法是挂载在 window 目标上的,settimeout履行时,履行回调中的 this 指向调用 settimeout的目标,所以是 window

优先级

  • 假如某个调用方位能够运用多条规矩该怎么办?为了解决这个问题就必须给这些规矩设定优先级。清楚明了,默许绑定的优先级是四条规矩中最低的。

function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 666,
  foo,
};
var obj2 = {
  a: 777,
  foo,
};
obj1.foo(); // 666
obj2.foo(); // 777
obj1.foo.call(obj2); // 777
obj2.foo.call(obj1); // 666
  • 经过以上代码能够看到,显现绑定隐式绑定 优先级更高,也便是说在判别是应当先考虑是否能够存在显现绑定。

function foo(age) {
  this.age = age;
}
var obj1 = {
  foo,
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.age); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.age); // 3
var bar = new obj1.foo(7);
console.log(obj1.age); // 2
console.log(bar.age); // 7
  • 能够看到 new绑定隐式绑定 优先级更高,可是 new绑定显现绑定 谁的优先级更高呢?

  • 由于 newcall/apply 无法一同运用,因而无法经过 new foo.call(...) 来直接测验,可是咱们能够运用硬绑定来测验他俩的优先级。


function foo(age) {
  this.age = age;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.age); // 2
var baz = new bar(3);
console.log(obj1.age); // 2
console.log(baz.age); // 3
  • 成果出人意料,bar 被绑定到 obj1 上,可是 new bar(3) 并没有像咱们语句的那样把 obj1.age 修改为 3。相反, new 修改了硬绑定 (到 obj1的) 调用 bar(...)中的this。
  • 这是由于 new 调用时bind之后的函数,会疏忽 bind 绑定的第一个参数,稍后咱们会用 bind 办法的 ployfill 完成来讲清楚为什么会这样。
  • 综上所述,它们的优先级顺序分别是:
  1. new 调用;
  2. callapplybind 调用;
  3. 隐式绑定(目标办法调用);
  4. 默许绑定(一般函数调用);

bind的ployfill完成


Function.prototype.Bind = function (pointer) {
  if (typeof this !== "function") {
    throw new TypeError(
      "Function.prototype.bind - what is trying to be bound is not callable"
    );
  }
  // 将参数转换为数组
  const args = Array.prototype.slice.call(arguments, 1);
  const self = this;
  const NewFunc = function () {};
  const fBound = function () {
    return self.apply(
      // 假如是 new 操作符,则从头绑定this
      this instanceof NewFunc && pointer ? this : pointer,
      args.concat(Array.prototype.slice.call(arguments))
    );
  };
  NewFunc.prototype = this.prototype;
  fBound.prototype = new NewFunc();
  return fBound;
};
  • 其间,下面便是 new 修改 this 的相关代码:

this instanceof NewFunc && pointer ? this : pointer;
// ... 以及;
NewFunc.prototype = this.prototype;
fBound.prototype = new NewFunc();

软绑定

  • 之前咱们讲到,硬绑定这种办法能够把 this 强制绑定到指定的目标(除了运用 new时),防止函数调用运用默许绑定规矩。
  • 可是问题就在于硬绑定会大大降低函数的灵活性,运用硬绑定之后就无法运用隐式绑定或许显现绑定来修改 this 的才能,详细来看完成:

Function.prototype.softBind = function (object) {
  let fn = this;
  // 捕获所有的curried参数
  const curried = [].slice.call(arguments, 1);
  const bound = function () {
    return (
      fn.apply(!this || this === (window || global) ? object : this),
      curried.concat.apply(curried, arguments)
    );
  };
  bound.prototype = Object.create(fn.prototype);
  return bound;
};
function foo() {
  console.log(this.name);
}
const obj = {
  name: "obj",
};
const obj2 = {
  name: "obj2",
};
const obj3 = {
  name: "obj3",
};
const fooOBJ = foo.softBind(obj);
fooOBJ(); // obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // obj2
fooOBJ.call(obj3); // obj3
setTimeout(obj2.foo, 1000); // obj
  • 能够看到,软绑定版别的 foo() 能够手动的将 this 绑定到不同的目标上。

参考文章

  • 书籍 你不知道的JavaScript 上卷
  • MDN

结尾

  • 一个小小的 this指向,就涵盖了new、call、apply、bind,箭头函数等用法。然后扩展到效果域、闭包,原型链,承继、严厉形式,这实力不容小觑。