javaScript 的 this 究竟是个什么鬼?

整理自《你不知道的JavaScript(上卷)》

一、what 和 why

this 是个什么东西?它是 运行时绑定的 一个记录属性,上下文取决于函数调用时的各种条件。看看书里的具体解释是什么:

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

为什么要使用 this?为了引用合适的上下文对象。 什么是“合适”而什么又是“上下文对象”我们将在下文中展开探讨。

二、绑定位置

既然是为了引用合适的上下文对象,我们得把这个 this 放在合适的地方绑定。接下来介绍四种常见的绑定规则。

1、默认绑定

就是最最一般的情况啦,看下面的例子。foo 函数是绑定在全局对象上的(也就是想象最顶层有一个全局对象,foo 是它身上的一个函数挂件),调用的时候 this 就会指向全局对象中的那个 a ,也就是外面的那个 2 。

在严格模式下会报错 undefined

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

2、隐式绑定

隐式绑定也是一种非常常见的绑定形式,个人觉得是更通俗意义上的默认绑定方式。牢记一句话:

this 绑定在运行时最后一个调用它的对象上

如果隐式绑定丢失了自己的绑定对象,便会执行默认绑定,将函数绑定在 window 全局对象上。

看下面几个例子。

function foo(){
  console.log(this.a);
}
var obj1 = {
  var a = 42,
	foo: foo
}
var obj2 = {
  var a = 21;
	obj1: obj1
}
obj2.obj1.foo() // 42

------------------------------------------

var bar = obj1.foo();
var a = "okok 我最ok"
bar() // "okok 我最ok"

------------------------------------------

function doFun(fn){
  fn()
}
doFun(obj1.foo) // "okok 我最ok"

这个例子中第一个输出语句(第12行)最后调用 foo 函数的是 obj1 对象,因此 this 是指向 obj1 中的 a 这个变量的,输出42,没毛病。第二个输出语句(第15行)的函数 bar 是孤零零直接被调用的,那么就会直接绑定到最外面的那个 window 对象,挂在 window 对象上的 a 是啥呢?就是"okok 我最ok"。即使 bar 是由 obj1 赋值而来,那都不用看,不管前面有多少花里胡哨的赋值调用,只需要看最后运行时是谁调用它就行。

第三个输出语句有点怪异,上面不是说看运行时谁调用它吗?这下是 obj1 调用的,为什么输出的是全局对象上挂载的变量呢?

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

这句话就是说,可以把 obj1.foo 当作一个整体(也就是函数 foo)赋值给了 doFun,那么在调用 doFun 的时候,实际上还是挂载在最外层的全局对象 window 上了,因此仍然输出了“ok”语句,并没有违反最终运行调用的规则。

3、显式绑定

显式绑定的意思是用一些方法在某个对象上强制调用函数。比如使用 call、apply 和 bind 显式绑定 this 的上下文对象。那么接下来就不得不整理一下这三者的区别了。

callapplybind
funcA.call(obj, arg1, arg2, …)funcA.apply(obj, [args…])funcA.bind(obj)
在调用 funcA 函数时,funcA 的this 指向传入的第一个参数,即 obj,后面的参数为调用 funcA 的其他参数【立即执行】和 call 基本一致,只是传参的形式不同,apply 将其他参数合并为数组传入【立即执行】bind 只是作绑定,与前两个一样,funcA 执行的时候会将 obj 作为 this 的指向值,不同点在于它返回的是一个函数而不是执行结果【手动执行】

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。

这句话是什么意思呢?首先应该为所谓“丢失绑定”下一个定义,因为前面说了,this 会指向最后一个调用它的对象,那么这个 this 的指向将会是飘忽不定的。比如说在回调函数中,虽然一开始使用上述显式绑定的方法明确了 this 的指向,但在异步操作结束执行回调的时候,这个 this 可就不一定顺着我们的心意了。所以我们需要下面的办法:硬绑定。

3.1 硬绑定

搞几个例子看看。

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

---------------------------------------------------------------------

function bind(fn, obj){
  return function(){
    return fn.apply(obj, arguments);
  };
}

var obj = {
  a: 2
};

var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5

上述结果都正确地输出了我们的理想值,那它为什么叫“硬绑定”呢?因为在 bar 函数的内部,foo 的 this 被强制(显式)绑定到了 obj 上,后面的函数不论怎样调用它都能执行我们理想的结果。

而上面第二个例子中的 bind ,成为了ES5就提供的内置方法 Function.prototype.bind。面试官再让你手写 bind 的时候,就应该信手拈来啦~

4、new 绑定

最后一种改变 this 指向的方法了,加油!

在 js 中,构造函数并不会实例化一个类或者创建一个类,当我们使用 new 操作符的时候,只是调用了一些普通的函数,这个机制与其他语言不一样。即书中所说:

不存在所谓“构造函数”,只有对函数的“构造调用”。

当我们使用 new 来调用函数时,会发生以下几件事:

  1. 创建一个全新的对象;
  2. 新对象会被执行 [[ prototype ]] 连接;
  3. 新对象和函数调用的 this 绑定;
  4. 执行构造函数中的代码(如果有);
  5. 如果函数没有其他返回对象则返回这个新创建的对象。

举个例子:

function Mother(name){
  this.name = name;
}
var son = new Mother("Da");
  1. 创建一个新对象 son;
  2. 执行 [[ prototype ]] 连接,son.__proto__ = Mother.prototype;
  3. 新对象和函数调用 this 绑定:Mother.call(son, "da");
  4. 执行构造函数 son.name
  5. 返回新对象 son

三、优先级

一般而言

废话不多说:显式绑定 > 隐式绑定 > 默认绑定,如果是 new 绑定,则会创建一个新的对象。

  1. 函数用 new 调用了吗?是——创建新的对象
  2. 函数有用 call、apply 这样的显式绑定吗?是——显式绑定时候的 this
  3. 函数有上下文吗?是——根据上文“隐式绑定”的内容看最后调用的对象即为 this 指向
  4. 啥也没有,挂在 window 上

特殊情况

  1. 显式绑定会忽略传入为 null 、undefined 的情况,应用默认绑定规则;
  2. 软绑定 —— 请看下面的示例
 if (! Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
    var fn = this;
    // 捕获所有 curried 参数
    var curried = [].slice.call (arguments, 1);
    var bound = function() {
    return fn.apply(
       (! this || this === (window || global)) ?
          obj : this,
          curried.concat.apply(curried, arguments)
       );
    };
    bound.prototype = Object.create(fn.prototype);
    return bound;
    };
}

可以给默认绑定指定一个全局对象和undefined以外的值,同时保留隐式绑定或者显式绑定修改this的能力

关键语句是 !this || this === (window || global) ? obj : this,即当发现传入的 this 为空或者是全局对象 window 时,将 this 指向默认的对象,其他时候保留调用时的 this。

书中还提到了函数柯里化,这块内容可以参考 一些js的奇淫技巧 中关于函数柯里化的部分。其实个人觉得软绑定也属于一种技巧型的操作。


贴一下语雀原文链接,阅读体验更佳
欢迎交流!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值