理解JS的函数调用和‘this’的指向

栏目: JavaScript · 发布时间: 5年前

内容简介:原标题:JS的函数调用一直以来给不少人带来疑惑,其中在我看来,首先理解了函数调用的原始核心语法,然后弄清楚其他调用函数的语法糖,这些疑惑就能解决了。实际上这正式ECMA规范所设计的思路。在某种程度上,这篇文章是ECMA规范的简化版,不过基本理念都是一样的。

原标题: Understanding JavaScript Function Invocation and "this" ,出自Yehuda的这篇博客,是在Typescript的中文教程里看到的。

JS的函数调用一直以来给不少人带来疑惑,其中 this 的语义是人们抱怨的最多的。

在我看来,首先理解了函数调用的原始核心语法,然后弄清楚其他调用函数的语法糖,这些疑惑就能解决了。实际上这正式ECMA规范所设计的思路。在某种程度上,这篇文章是ECMA规范的简化版,不过基本理念都是一样的。

核心源码

首先来看JS函数调用的核心, Function 类的 call 方法【1】。 call 方法的逻辑很直白:

  1. 把从第二个起的所有参数放进一个参数列表,如 argList
  2. 把第一个参数定为 thisValue
  3. 执行 function ,把 this 指向 thisValueargList 作为参数列表

例如:

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

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

可以看到,在执行 hello 函数时我们把 this 指向 "Yehuda" ,传入单个参数 "world" 。这就是JS函数调用的核心源码。你可以把其他的函数调用的语法都看成是这个源码的语法糖。

【1】在ES5规范中, call 还是另一个更低层次的源码的语法糖,但包装得并不复杂,所以在这里直接简化了。文末有更多资料。

简单的函数调用

显然每次都用 call 来调用函数太累赘了。JS允许我们直接使用括号来调用函数,如 hello("world") ,这个就是一个语法糖了:

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

// this:
hello("world")

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

在ES5的严格模式(strict mode)下,有一点小小的改动:【2】

// 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】实际上原作者说他撒了个小谎。ES5规范说的是给 thisValue 所绑定的几乎都是 undefined (The ECMAScript 5 spec says that undefined is (almost) always passed),但他认为不在严格模式时 thisValue 应该绑定到 global 对象。 This allows strict mode callers to avoid breaking existing non-strict-mode libraries.

成员函数

另一个常见的场景是调用一个对象的成员函数,如 person.hello() 。这时候函数调用的语法糖分析如下:

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

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

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

要注意,无论 hello 函数是如何添加到这个对象的,效果都是一样的,记得事先声明一个独立的 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 指向,开发者会使用闭包来实现这个目的:

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("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"
复制代码

要理解这段代码,你只需直到另外两个信息:首先, arguments 是一个类数组对象,表示所有传给这个函数的对象;其次, apply 的作用和 call 类似,但前者一次接收一个类数组对象作为传参,后者接收多个参数。

这里的 bind 函数简单返回一个新的函数。在调用 bind() 时,它又会调用之前传参进去的函数,并且把后者的 this 绑定到第二个参数。

因为这种用法也很常见,所以ES5引入了一个新的方法 bind ,适用于所有 Function 类对象,效果如下:

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

如果你需要写一个(带有 this ,但其指向有特定需要的)回调函数,这种写法就很有用:

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

本段不做翻译。

后记(编者作)

有好几处的描述我对原本的规范描述做了简化,其中最关键的一点是我把 func.call 称为源码(primitive)。实际上在规范里, function.call[obj.]func() 还有更深一层的原源码。

但来看一下ES5标准中 func.call 的声明(译者:即ES5标准里 Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] ) 的描述):

  1. 如果 IsCallable(func)false ,抛出 TypeError 错误
  2. argList 初始化为空数组
  3. 如果这个函数传入了多个参数,将这些参数从左往右加入到 argList ,从 arg1 开始标记
  4. 返回调用 func 内置方法 [[call]] 的结果,调用时把 thisArg 赋给 this ,传入 argList 作为参数队列

可以看到,这只是一段很简单的绑定到 [[call]] 操作的JS代码。

如果你去看调用函数的定义,头7步都是在初始化 thisValueargList ,最后一步是“返回调用 func 内置方法 [[call]] 的结果,调用时把 thisArg 赋给 this ,传入 argList 作为参数队列( Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values. )”

一旦 argListthisValue 准备好之后,之后的工作原理都是一样的了。

所以我偷了个小懒,但我把ES5规范里的描述都拎出来了,他们的意义都是一样的。

还有一些其他用法,比如跟 with 相关的用法,在这里我没有涉及。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

群体智能

群体智能

James Kennedy、Russell C Eberhart、Yuhui Shi / 人民邮电出版社 / 2009-2-1 / 75.00元

群体智能是近年来发展迅速的人工智能学科领域.通过研究分散,自组织的动物群体和人类社会的智能行为, 学者们提出了许多迥异于传统思路的智能算法, 很好地解决了不少原来非常棘手的复杂工程问题.与蚁群算法齐名的粒子群优化(particle swarm optimization, 简称PSO)算法就是其中最受瞩目,应用最为广泛的成果之一. 本书由粒子群优化算法之父撰写,是该领域毋庸置疑的经典著作.作者......一起来看看 《群体智能》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器