【译】理解 JavaScript 中函数调用和 this

原文 Understanding JavaScript Function Invocation and "this"
github 的地址 欢迎 star!
javascript

前言

过去几年,我常常听到不少人对 JavaScript 函数调用的谈论,尤为是对其中 this 指向是困惑的。html

在我看来,经过深刻了解函数调用的核心概念,这些困惑都是能够消除的,其余形式的调用都是其核心的语法糖。事实上,ECMAScript 规范就是这样认为的。这篇博客在不少地方其实就是简单的规范而已,不过基本概念都是相通的。java

The Core Primitive(核心原始地调用)

首先,来看一下核心原始地调用:函数 call 的方法[1]。call 的方法相对直接明了。来看一下过程:git

  1. call 括号后面的集合就是参数 list(从第一个参数到最后一个):arguments
  2. 第一个参数就是 thisValue
  3. 函数调用的时候,将函数的 this 指向这个 thisValue,除了 thisValue 之外的 arguments 当作函数的参数

例如:github

function hello(thing) {
  console.log(this + " says hello " + thing);
}

hello.call("Yehuda", "world") //=> Yehuda says hello world
复制代码

如你所见,调用 hello 方法是把 this 指向 "Yehuda",同时给它传递了一个简单 "world" 的参数。这就是核心原始的函数调用的形式。你能够认为其它全部的函数调用其原理都是经过 call 的形式来实现的(其它形式都是 call 的语法糖,语法糖是指用一个更方便语法和一个更基本的核心原生术语描述它)面试

[1] 在 ECMAScript 5规范中,call 方法用另一种更加底层的原生的方法描述。可是它真是一个很是轻的包装, 因此我在这里简化了一点。想了解更多信息请看文章末尾。设计模式

简单的函数调用

明显地,每次都用 call 方法调用函数有点烦人。全部 JavaScript 容许咱们经过这种模式的语法 hello("world") 调用函数。当咱们这样调用时,它内部语法仍是用 call 的形式数组

function hello(thing) {
  console.log("Hello " + thing);
}

// this:
hello("world")

// desugars to:  上面的内部实现
hello.call(window, "world");
复制代码

固然上面的形式在 ECMAScript 5的严格模式下是不一样的[2]bash

// this:
hello("world")

// desugars to:
hello.call(undefined, "world");
复制代码

通用公式: fn(...args) <==等价于==> fn.call(window [ES5-strict: undefined], ...args)闭包

注意,对于当即执行的匿名函数也是如此:

(function() {})()
// 等价于
(function() {}).call(window [ES5-strict: undefined)
复制代码

[2]实际上,我撒了点谎。 ECMAScript 5 规范说了应该所有都是 undefined 传递的,但在非严格模式须要把 this 指向全局对象。这样作是为了不在严格模式下调用了非严格模式的三方库致使异常的状况

对象方法进行调用

接下来常见的方法,调用一个对象的方法如 person.hello()。在这种状况下,调用语法糖以下:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this + " says hello " + thing);
  }
}

// this:
person.hello("world")

// desugars to this: 内部实现,this指向那个对象
person.hello.call(person, "world");
复制代码

注意:在这种状况下 hello 方法和对象的是如何绑定的并不重要。还记得咱们以前定义了一个独立的 hello 函数, 让咱们看看把它动态的绑定在对象上会发生什么:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world")

hello("world") // "[object DOMWindow]world"
复制代码

注意:函数中的 this 不是固定的(非箭头函数)。它老是在调用过程当中由谁调用它来肯定的。

使用Function.prototype.bind

固然,有时候仍是须要 this 的值是固定的,一般使用一个简单的闭包就能够固定住 this 的指向:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var boundHello = function(thing) { return person.hello.call(person, thing); }

boundHello("world");
复制代码

即便使用boundHello.call(window, "world")这样原始的调用形式,咱们也没法达到改变 this 的预期。

咱们能够作一些改动使它更通用:

var bind = function(func, thisValue) {
  return function() {
    return func.apply(thisValue, arguments);
  }
}

var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"
复制代码

为了理解 this,你只须要知道这两个点。首先 arguments 是一个类数组的对象,它表明了传递给函数的全部参数。第二,apply 的方法准确地说像 call 的底层实现,只是把那个类数组对象用一个接一个参数代替。

咱们 bind 的方法简单地返回了一个新的函数。当它被调用的时候,新的函数又调用原始函数,并把 this 指向原始值,它也能够传递参数。

由于这个方法是经常使用的,故 ES5 给全部 Function 对象定义了一个新的方法 bind,实现了以下调用:

var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"  this都是指向person的
复制代码

这是很是实用的,当你把一个原始函数做为一个回调的时候:

var person = {
  name: "Alex Russell",
  hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed
复制代码

固然这种方式是笨重的, TC39(正在编写下一代 ECMAScript 的组织)创造一种更加优雅和 向后兼容的解决方法(箭头函数)。

在jQuery中

由于 jQuery 使用了不少的匿名函数,在内部它使用 call 将 this 指向了预期有用的值。例如,在 jQuery 的全部事件操做中(假设你没有进行特殊的操做),DOM 调用回调函数,this 老是指向那个 DOM 元素。

这是很是有用的,由于在匿名函数中默认的 this 是没有什么特殊做用的。但这使得那些刚刚学习 JavaScript 的人将难以理解 this。

若是你明白将函数调用改写为 func.call(thisValue, ...args) 这样简单的方法(去除语法糖),将不会在肯定JavaScript中this值的过程当中迷失。

PS(附言): 我撒谎了

在不少地方,我稍微简化了规范里面一些确切的点。最明显的地方就是我将 func.call 的方法看成一个原生函数调用方法。事实上,规范里面明确了 func.call[obj.]func() 都是经过一个叫 [[Call]] 的原始方法实现的。

固然,看看 fun.call(thisArg, arg1, arg2, ...) 的定义:

  1. 若是不能当作函数调用,则抛出类型错误。
  2. 参数列表为空
  3. 若是传了不止一个参数,则按从左到右的 arg1, arg2 的顺序把这些值传递给函数做为参数列表(除了 this )
  4. 使用调用者提供的this值和参数调用该函数的返回值。this 指向 thisArg,arguments 指向后面的参数组成的 list

如上所述,这个定义本质就是一个简单的 JavaScript 绑定原始 [[Call]] 的操做。

你能够回顾一下这个函数调用的定义,首先几步是创建 thisValueargList,把 this 指向 thisValue, 把 arguments 指向了 argList,最后一步,调用内部函数,返回结果。这和那个原始的最初的调用是基本相同的。

我撒谎称 call 是最原始的函数调用方式,但它的内部调用实现基本和规范里面说的原始调用形式是相同的。

另外还有一些额外的 this 指向的例子没有说明,像 with。

额外的总结

看到了《JavaScript 设计模式与开发实践的总结

除去不经常使用的 with 和 eval 的状况,具体到实际应用中,this 的指向大体能够分为如下4种:

  1. 做为对象的方法调用:指向函数直接所在的那个对象
  2. 做为普通函数调用(this 就是改写为 call 的第一个参数)
  3. 构造器调用( new Fn() ):指向新生成的那个对象实例(固然书中说了,构造函数里面要避免显式的返回一个对象,否则 this 指向的是返回的那个对象!)
  4. Function.prototype.call 或 Function.prototype.apply 调用

此外,还有箭头函数中,this 就是箭头函数相邻外面的那个this(也是一个参数)

固然你能够看一下 call 的实现,你就能够理解一下函数核心原始的底层调用是什么样子:

Function.protoType.call2 = function(context){
    var context = context | window;// 可能没有传参数
    context.fn = this;
    var args = [];
    for(var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i +"]"); // 不这么作的话 字符串的引号会被自动去掉 变成了变量 致使报错
    }
    args = args.join(",");
    
    var result = eval("context.fn(" + args +")");//至关于执行了context.fn(arguments[1], arguments[2]);
    
    delete context.fn;
    return result;
}
复制代码

另外强烈推荐你们能够看一下如何编写高质量的函数 -- 敲山震虎篇 ---详细介绍了函数中底层知识: 总结以下:

  1. 建立函数,开辟堆内存,以字符串存入函数体,将函数名(变量)的值变为函数体堆内存中地址。
  2. 执行函数,将存储的字符串函数体复制一份到新开辟的栈内存中,使其变为真正的 JS 代码

反正我就记住了,this 是一个参数,是一个参数!

若是有错误或者不严谨的地方,请务必给予指正,十分感谢!

参考

  1. www.ruanyifeng.com/blog/2018/0…
  2. Understanding JavaScript Function Invocation and "this"
  3. 【周刊-1】三年大厂面试官-面试题精选及答案--call 实现
  4. 如何编写高质量的函数 -- 敲山震虎篇
相关文章
相关标签/搜索