理解JavaScript函数调用和"this"

作者:Yehuda Katz
原文链接: http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/
多年以来,我看到了许多人对于JavaScript函数调用有很多困惑。特别是许多人会抱怨,"this"在函数调用中的语义是令人疑惑的。
在我看来,通过理解核心的函数调用的原始模型,并且去看一下在此基础之上的其他方式的函数调用(对原始调用的思想的抽取)可以消除这些困惑。实际上,ECMAScript 标准也是这么考虑的。在某些地方来看,这篇文章是标准的简化,但是二者的基本思想是一致的。
 

核心的原始函数调用方法

首先,让我们来看一下核心的函数调用原始模型,一个Function的call方法[1]。call方法相对比较直接。
1、取参数的第一个到最后一个组成一个参数列表(argList);
2、第一个参数是thisValue;
3、把this设置为thisValue同时argList作为它的参数列表来调用函数。

比如:

function hello(thing) {
  console.log(this + " says hello " + thing);
}
 
hello.call("Yehuda", "world") //=> Yehuda says hello world

正如你所见,我们调用了hello函数,把this设置为"Yehuda" 并传入了一个参数"world"。这是JavaScript函数调用的主要原始形式。你可以把所有其他的函数调用作为这个原始模式的运用来考虑。(要“运用”原始模型来调用其他函数就要用更便利的语法并依据一个更基本的主要原始模型)
注:[1]在ES5标准中,call方法的描述基于其他的,更低水平的基元,但是它是在那个基元基础上的非常简单的包裹,因此我在这里将其简化了。想了解更多可以参考这篇文章后面的信息。
 

简单的函数调用

很明显,总是用call来调用函数是令人难以忍受的。JavaScript允许我们用括号语法来直接调用函数(hello("world"))。当我们这么做的时候,调用是这样的:

function hello(thing) {
  console.log("Hello " +thing);
}
 
// this:
hello("world")
 
// desugars to:
hello.call(window, "world");

在ECMAScript 5 中,在严格模式下这个行为已经发生了变化[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] 实际上,我撒了点谎。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:
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 方法仍然可以改写为boundHello.call(window, "world") ,我们转换了一个角度,应用我们的基元call方法来改变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方法简单的返回一个新函数。当它被调用的时候,我们的新函数简单的调用传进来的原始函数,设置原始值为this。它也遍历参数。
因为this在某种程度上是一个常见的习语,ES5引入了一个新的bind方法给所有的Function对象来实现下面的行为:

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

当你需要一个未加工的函数作为回调函数的时候这是非常有用的:

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里面的bind

因为jQuery里面大量的应用匿名回调函数,它内部使用call方法来设置那些回调函数的this值为更有用的值。比如,在所有的事件处理器函数中,jQuery没有接收window作为this的值(如果你没有特殊的干预),而是对元素调用call方法,并将事件处理器函数作为第一个参数。
这极其有用,因为在匿名函数内部的this的默认值并不是特别有用,但是它会给JavaScript初学者一个这样的感觉:this一般是很奇怪的,并且是难以推测的经常变化的一个概念。
如果你理解了从一个有语法糖的函数调用到抽取出了“糖分”的函数调用func.call(thisValue, ...args)的基本转换规则,你应该就能操纵这个并不是十分“阴险”的 JavaScript this 值这一领域。

this-table.png

附:我‘欺骗’了你

在几个地方,对于规范的措辞我有所简化。或许最重要的‘欺骗’是我将func.call称为一个基元("primitive")。实际上,这个规范有一个基元(在内部被称为[[Call]])为func.call和obj.]func()所共有。
然而,让我们来看一下func.call的定义:
1、如果IsCallable(func) 结果为false,那么就抛出一个类型异常;
2、让 argList  为一个空列表;
3、如果这个方法被调用的时候参数不止一个,那么从左到右开始将arg1追加每一个参数作为 argList 的最新元素;
4、返回调用func的内部方法[[Call]]的执行结果,提供thisArg作为this的值,argList作为参数的列表。
 
正如你所见,这个定义本质上是一个很简单的JavaScript的语言绑定到基元[[Call]]操作符。
如果你看一下函数调用的定义,前七步是设置thisValue和argList,最后一步是:“返回 调用func的内部方法 [[Call]]的结果值,提供thisArg作为this的值,argList作为参数的列表”。
一旦thisValue和argList的值被确定,func.call的定义和函数调用的定义本质上是相同的字眼。
我在称call为一个基元上做了一点欺骗,但是在本质上他们意思还是一样的,我在文章开头拿出规范且做了引用。
还有很多案例(大多数文章会明显的包含with)我没有在文章中进行讨论。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与国已互相成为双方最大的交易伙伴。国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占国外贸总值的15.4%。在过去20余年,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模式、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模式:东南亚市场销售模式、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值