JavaScript核心原理解析
周爱民
《JavaScript语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
立即订阅
3625 人已学习
课程目录
已更新 21 讲 / 共 21 讲
0/3登录后,你可以任选3讲全文学习。
开篇词 (1讲)
开篇词 | 如何解决语言问题?
免费
从零开始:JavaScript语言是如何构建起来的 (5讲)
01 | delete 0:JavaScript中到底有什么是可以销毁的
02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题
04 | export default function() {}:你无法导出一个匿名函数表达式
05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销
从表达式到执行引擎:JavaScript是如何运行的 (6讲)
06 | x: break x; 搞懂如何在循环外使用break,方知语句执行真解
07 | `${1}`:详解JavaScript中特殊的可执行结构
08 | x => x:函数式语言的核心抽象:函数与表达式的同一性
09 | (...x):不是表达式、语句、函数,但它却能执行
10 | x = yield x:迭代过程的“函数式化”
11 | throw 1;:它在“最简单语法榜”上排名第三
从原型到类:JavaScript是如何一步步走向应用编程语言的 (6讲)
12 | 1 in 1..constructor:这行代码的结果值,既可能是true,也可能是false
13 | new X:从构造器到类,为你揭密对象构造的全程
14 | super.xxx():虽然直到ES10还是个半吊子实现,却也值得一讲
15 | return Object.create(new.target.prototype):做框架设计的基本功:写一个根类
16 | [a, b] = {a, b}:让你从一行代码看到对象的本质
17 | Object.setPrototypeOf(x, null):连Brendan Eich都认错,但null值还活着
不定期加餐 (3讲)
加餐 | 捡豆吃豆的学问(上):这门课讲的是什么?
免费
加餐 | 捡豆吃豆的学问(下):这门课该怎么学?
免费
加餐 | 让JavaScript运行起来
免费
JavaScript核心原理解析
登录|注册

08 | x => x:函数式语言的核心抽象:函数与表达式的同一性

周爱民 2019-11-29
你好,我是周爱民。
在运行期,语句执行和特殊的可执行结构都不是 JavaScript 的主角,多数情况下,它们都只充当过渡角色而不为开发人员所知。我相信,你在 JavaScript 中最熟悉的执行体一定是全局代码,以及函数
而今天,我要为你解析的就是函数的执行过程
如同在之前分析语句执行的时候与你谈到过的,语句执行是命令式范型的体现,而函数执行代表了 JavaScript 中对函数式范型的理解。厘清这样的基础概念,对于今天要讨论的内容来说,是非常重要和值得的。
很多人会从对象的角度来理解 JavaScript 中的函数,认为“函数就是具有 [[Call]] 私有槽的对象”。这并没有错,但是这却是从静态视角来观察函数的结果。
要知道函数是执行结构,那么执行过程发生了什么呢?这个问题从对象的视角是既观察不到,也得不到答案的。并且,事实上如果上面这个问题问得稍稍深入一点,例如“对象的方法是怎么执行的呢”,那么就必须要回到“函数的视角”,或者运行期的、动态的角度来解释这一切了。

函数的一体两面

用静态的视角来看函数,它就是一个函数对象(函数的实例)。如果不考虑它作为对象的那些特性,那么函数也无非就是“用三个语义组件构成的实体”。这三个语义组件是指:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《JavaScript核心原理解析》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(10)

  • kittyE
    我理解 x 这个单值表达式 是不是可以等价于 x => x; 的计算过程?毕竟直接return了 不知道理解的对不对

    作者回复: 非常赞。这个答案其实比我想的还要好。^^.

    2019-11-30
    4
  • 海绵薇薇
    Hello 老师好:)

    一直以来各大博客描述的包括我理解的闭包是:函数可以访问函数作用域外标识符的能力。但是本文描述闭包的角度完全不一样。这两者有联系吗?

    作者回复: "函数可以访问函数作用域外标识符的能力",这个与闭包没关系,而是scope-chains的事情。不过现在这个概念也改了,在ES6之后,这里被称为环境/作用域。所以,如果仅仅是在概念层面讨论这个话题,比较容易变形。

    回到提出这个概念的早期,在ES3~ES5的那个时代,概念比较简单清晰一些。有一个东西称为“对象闭包(object closure)”,这个东西写在spidermonkey的代码里面,也写在一个称为narcissus的开源项目里面,尤其是narcissus,它是JS之父本人Eich一直在维护的一个项目,也就是说,是一种正统的“概念”。——我说这一段是想说明,我下面的说法不是心血来潮随口胡诌。

    Ok。什么是对象闭包呢?所谓对象闭包,就是“with (x) ...”中打开的那个用`x`的属性表来形成的闭包。所以,如果(注意这个推论哟),如果对象闭包是一种闭包,那么要么是概念上属性表等义于闭包,要么是闭包是一种属性表。

    OVER。^^.

    2019-12-02
    2
  • 海绵薇薇
    Hello,老师好:)

    关于函数闭包又有了新的理解,函数形成的闭包是函数本身 + 函数当前的环境的集合。也就是说函数表达式的Result(闭包母本)中应该包含当前函数的环境(词法环境/执行逻辑的土壤)和函数体本身(对象/数据&逻辑)。

    作者回复: 赞的。^^.

    ```
    // f是函数x的一个实例
    f = function x() {}

    // 创建f的一个闭包、初始化该闭包并在其中执行代码
    f()
    ```

    2019-12-03
    1
  • 海绵薇薇
    Hello,老师好:),也不知道想问啥,感觉还是没有理清楚,望指点,问题如下:

    0.

    闭包是函数表达式的值?

    1.

    let c = function (a) {}

    c是闭包(上面匿名函数的实例),并且闭包中有标识符a(简单参数)

    let e = c

    e应该也是闭包,并且指向c(e === c 为true),也有标识符a

    现在看来e和c闭包中标识符a是同一个,但是不应该是同一个。应该是哪里有了根本性问题,导致这样的理解。

    2.

    函数一体两面,逻辑和数据。那么闭包指的是逻辑?数据?还是函数的另一个名字表示的逻辑和数据?

    作者回复: 先把你的第一个问题回复了,你试着再推演一下其它的。——你的第一个问题问得非常到位,是你思考上下文中的关键点。赞!

    “闭包是函数表达式的值?”答案即“是”,且“非”。

    首先说“是”的部分。单看ECMAScript这里:
    https://tc39.es/ecma262/#sec-function-definitions-runtime-semantics-evaluation

    你会发现,一个函数表达式执行之后,就是“Return closure”。所以,这个说法在ECMAScript层面是对的。

    但是我通常在这里说,这个过程得到的其实还只是一个“函数实例”。为什么呢,比如说:
    ```
    for (var arr = [], i = 0; i<10; i++) arr.push(function() {});
    ```
    这里有几个匿名函数在arr[]里面?答案是有10个。所以函数表达式每执行一次,就得到一个函数实例。你的示例`let c = function() {}`也一样,右操作数每执行一次就得到一个新的。

    但是这是函数实例。函数闭包其实是在它调用的时候才创建和初始化的。——只不过,所有的、因为函数调用而产生的闭包,都是上面这个实例的一个“副本”。所以,一个实例其实也可以有多个闭包。这里的意思是说,ECMAScript在上面把它叫闭包:既没错,它是所有其它闭包的“母本”;也不太对,因为它不是你在执行过程看到的任何一个闭包。

    ——一个函数实例可以有多个闭包,这很少见,但的确是存在的。比如递归,其实是一个函数的多次调用,那这个函数(实例)只有一个,闭包却是有多个了。

    2019-12-02
    1
    1
  • 许童童
    加油,希望以后来回顾的时候,可以回答出来上面三个思考题。
    2019-11-29
    1
  • ssala
    表达式也是3部分,如果以类比函数的思维来看待表达式,我认为:

    1. 表达式中用到的参数构成了表达式的“参数”
    2. 表达式本身构成了其逻辑体,这是表达式的逻辑部分
    3. 表达式的运算结果就是它的“返回值”

    单值表达式x在逻辑上等同于标题中的箭头函数。

    作者回复: 非常赞的思考!

    表达式和函数并没有本质不同,它们的抽象形式是一样的。事实上,如果你理解“当运算符等义于某个函数”时,那么你其实就看到了“函数式语言”范式的根本逻辑,关于这一点,我之前也讲过,在《JavaScript语言精髓与编程实践》中也是拿这个来引入函数式语言的。

    当运算符等义于函数,那么操作数就是入口参数,而运算结果就是函数返回。运算在本质上就是一个IO和一个处理。这样的理解会让你看到计算理论中最基础的那些模型。

    谢谢你提出这一点,真的很赞!

    2019-12-21
  • 子笺
    听完老师这一讲,我脑海中有幅构图:
    函数有几个基本要素:执行的上下文【素材】,执行的过程【对素材的加工】,执行的结果【成果】
    为了完成成果,必须要有特定的方式去处理整合素材,有合理素材处理流程
    首先是整合素材:考虑到整合素材时,有普通的参数传递方式,还有一些特殊的(缺省、解构),就用了2种形式去整合素材。
    然后是对素材的加工:结合我对编译的认知,这些素材都有各自的占位,在解析阶段,其实已经有了对各个占位的处理流水线,在执行阶段这些占位真正有了自己的实物,直接在已经构建的流水线上去做处理。
    最终可以拿到成果

    同理,我想表达式也差不多。
    在这节课之前才看了老师关于如何学习的加餐,不管对错,先有自己的理解

    作者回复: 赞!这是极难得的进展!
    值得再赞一次!!

    2019-12-20
  • 可可
    没有JavaScript基础,看的云里雾里的,是不是要买老师的新书来看看啊

    作者回复: 新书还没出呢。😂

    不过可以先看《程序原本》啊,免费开放的电子书。在这里:
    https://github.com/aimingoo/my-ebooks

    与本课程相关的章节,以及对应关系我在加餐里面已经讲过了。另外,JavaScript基础有一些是最好,没有的话也不用太紧张,这门课程主要不是讲JavaScript应用的,所以语言就算不太熟悉,也不算特别大的障碍。所以我一般只强调“要有语言经验”,以及“最好JavaScript已经入门”。

    2019-12-12
  • IAmFineThankS
    函数闭包是函数执行时创建的,可是按照闭包的变量存储位置是在堆里的前提,函数每次执行完后的变量不应该被删除掉,但是实际上函数每次调用完后函数内部的变量都会被GC回收掉,感觉有点不是很懂~

    作者回复: 这里你是按传统的语言来理解的。并且,即使按“传统的语言”来理解,也并不太正确。

    首先,并不是函数(或闭包)的变量存储在堆里。在一些语言中(例如Delphi),函数内的局部变量也是可以存放在栈上的,并且多数情况下就是如此。所以一些传统的语言概念和实现思路,并不见得总是对的。

    其次,JavaScript是解释执行的,闭包(这里反过来说,或者函数)中的变量与标识符并不是使用堆/栈来管理的。所以它的回收也不是堆与栈的机制,通常这基于引用计数,但在不同的引擎中可能存在区别,是难以一概而论的。但是其中所谓“闭包”,才是回收所基于的一些基础组件,闭包代表了对对象、数据以及其它东西的引用,并且闭包自身也被作为引用对象——所以“函数每次调用完后函数内部的变量”其实在JavaScript中并不总是会被GC掉,因为闭包可能被别的地方引用了。例如简单的想法:你在函数内部return arguments.callee,那么函数(作为闭包)就被外部的环境引用了,那么显然变量之类的也不能被GC掉了。

    总之,JavaScript的函数与传统语言中的函数有着很大的区别。从语言特性上来说,它的动态语言特性和函数式语言特性,都会给传统语言背景下的学习者带来很大的困扰。这也是我在开篇词里面一再强调“要放下既有知识包袱”的原因。

    2019-12-11
  • IAmFineThankS
    处理函数参数的过程与此完全相同:参数被创建成“可变绑定”,如果它们是简单参数则被置以初值undefined,否则它们就需要一个所谓的“初始器”来赋初值。也就是说,并非 JavaScript 要刻意在这里将它作为 var/let 变量之一来创建,而只是用户逻辑执行到这个位置的时候,所谓的“可变绑定”还没有来得及赋初值罢了。
    什么意思? 为什么突然就能得出结论来了

    作者回复: let与var都是可变绑定,它们的不同不在于自身的数据结构(它们在引擎层面都是一样的)。

    然而var是有初值的,而let是无初值的,这个是它们在创建的时候(或者在创建之后“随即”)发生的变化。进一步的,规范约定“不能访问一个没有初值的绑定”。因此,let就不能在它的赋值代码之前访问,而var则可以(因为它有初值),这是他们之间的差异的由来。

    对于函数参数这个例子。简单参数是赋初值的,所以表现起来跟var一样。——对照起来,非简单参数也是一个“可变绑定”,但是没有赋初值(所以表现起来跟let一样)。文章中在这里的叙述,是想强调“使这个参数表现得像var/let”并不是原始的目的,原始的状况是这里本来没有什么目的性,只不过由于undefined这个值(作为缺省值)被缺省参数占用了,所以没办法用作初值而已。

    2019-12-11
收起评论
10
返回
顶部