探究 JavaScript 语法特性:为什么函数是「一等公民」?

本文最后更新于:2023-04-08 17:09:22 UTC+08:00

什么是 JavaScript

定义

JavaScript(JS)是一种具有函数优先 (First-class Function) 特性的轻量级、解释型或者说即时编译型的编程语言。虽然作为 Web 页面中的脚本语言被人所熟知,但是它也被用到了很多非浏览器环境中,例如 Node.js、Apache CouchDB、Adobe Acrobat 等。进一步说,JavaScript 是一种基于原型(Prototype-based)、多范式、单线程的动态语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

这里的名词有点多……后面我们一个一个来解释。

JavaScript 与 ECMAScript

JavaScript 的核心语言是 ECMAScript,是一门由 ECMA TC39 委员会标准化的编程语言。

1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

在 ECMAScript 第 6 版(称为 ES6)之前,规范是几年发布一次,通常用它们的主要版本号来指代(ES3、ES5 等)。在 ES6 之后,规范以发布年份命名(ES2017、ES2018 等)。ES6 是 ES2015 的代名词[1]。ESNext 是一个动态名称,指的是撰写本文时的下一个版本。ESNext 中的特性更准确地称为提案,因为根据定义,规范尚未最终确定。

任何人都可以向 ECMA TC39 委员会提案,要求修改语言标准。提案仓库:https://github.com/tc39/proposals

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

  • Stage 0 - Strawman(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。

讲 JavaScript 就不得不讲 ES6,下文也会穿插 ES6 的一些特性。

First-class Function 函数优先

函数是 JavaScript 的一等公民。

函数优先定义:是一种把函数视为变量的编程语言,函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。

在 JavaScript 中,函数即对象 (Object),每个 JavaScript 函数实际上都是一个 Function 对象。

1
console.log((function(){}).constructor === Function); // true

1
2
3
4
5
6
7
8
9
10
function hello() {
return "Hello!"
}
function func(text) {
return function() {
console.log(text);
}
}
const f = func(hello());
f();

上面例子中 func() 函数返回了一个「匿名函数」,匿名函数还有另外一种创建方式:

1
2
3
(function (text) {
console.log(text);
})("Hello!");

第二个括号用于调用该匿名函数。

ES6 中,可以使用箭头函数使代码更简洁。

1
2
3
4
var hello = () => "Hello!";
// var hello = () => { return "Hello!"; }
var f = (text) => console.log(text);
f(hello());

匿名函数大量用于「回调函数」。回调函数指被传递给另一个函数作为参数的函数叫作回调函数。

1
2
3
4
function func(callback) {
// do something...
callback();
}

JavaScript 中典型的全局方法 setTimeout() 就使用了回调函数,该方法会在给定的时间段(以毫秒为单位)后调用函数或计算表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(code)
setTimeout(code, delay)

setTimeout(functionRef)
setTimeout(functionRef, delay)
setTimeout(functionRef, delay, param1)
setTimeout(functionRef, delay, param1, param2)
setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN)

// example:
setTimeout(() => {
console.log("Hello!");
}, 3000);

Prototype-based 基于原型

定义:基于原型编程是面向对象编程的一种风格和方式。在原型编程中,行为重用(在基于类的语言通常称为继承),是通过复制已经存在的原型对象的过程实现的。在这种风格中,我们不会显式地定义类 ,而会通过向其他类的实例(对象)中添加属性和方法来创建类,甚至偶尔使用空对象创建类。

简单来说,这种风格是在不定义 class 的情况下创建一个 object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 「无中生有」对象
var foo = { name: "foo", one: 1, two: 2 };
var bar = { three: 3 };

bar.__proto__ = foo; // foo现在是bar的原型。

// 尝试从bar访问foo的属性
bar.one // 1

// 子对象的属性也是可访问的。
bar.three // 3

// 自身的属性遮蔽原型属性。
bar.name = "bar";
foo.name; // foo
bar.name; // bar

当我们调用一个对象的属性时,如果对象没有该属性,JavaScript 解释器就会从对象的原型对象上去找该属性,如果原型上也没有该属性,那就去找原型的原型,直到最后返回 null 为止,null 没有原型。这种属性查找的方式被称为原型链 (prototype chain)。

所有 JavaScript 中的对象都是位于原型链顶端 Object 的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Test = function () {
this.propA = 1;
this.methodA = function() {
return this.propA;
}
}

Test.prototype = {
methodB: function() {
return this.propA;
}
}

var objA = new Test();

objA.methodA(); // 1
objA.methodB(); // 1

console.log(objA);
// { propA: 1, methodA: [Function (anonymous)] }
console.log(objA.__proto__);
// { methodB: [Function: methodB] }

若此时再实例化一个新的变量:

1
2
3
4
5
6
7
8
var objB = new Test();

// Quiz: 会输出什么?
console.log(objA === objB);
console.log(objA.methodA === objB.methodA);
console.log(objA.methodB === objB.methodB);

console.log(Array.prototype.__proto__.toString === Object.prototype.toString);

严格相等比较 === (strict equality) 用于比较两个值是否全等,包括值与类型。

Dynamic Typing 动态类型

动态类型定义:解释器在运行时根据变量当时的值为变量分配类型的语言。

动态编程语言定义:一类在运行时可以改变其结构的语言,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。动态语言目前非常具有活力。

JavaScript 是一种具有动态类型的动态语言。

1
2
3
var foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean

JavaScript 也是一种弱类型语言,这意味着当操作涉及不匹配的类型时它允许隐式类型转换,而不是抛出类型错误。

1
2
3
var foo = 42; // foo is a number
var result = foo + "1"; // result is a string
console.log(result); // 421

nullundefined

从概念上讲,undefined 表示没有任何值,null 表示没有任何对象。以下使一些 undefined 的情况:

  • 没有值的 return 语句(即 return;),隐式返回 undefined
  • 访问不存在的对象属性(obj.iDontExist),返回 undefined
  • 变量声明时没有初始化(var x;),隐式初始化为 undefined
  • 许多如 Array.prototype.find()Map.prototype.get() 的方法,当没有发现元素时,返回 undefined。

null 在核心语言中使用频率少得多。最重要的地方是原型链的末端,其次是与原型交互的方法 (methods that interact with prototypes),如 Object.getPrototypeOf()Object.create() 等,接受或返回 null 而不是 undefined

可选链运算符 ?.

可选链运算符 ?. 允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 运算符的功能类似于链式运算符 .,不同之处在于,在引用为空(null 或者 undefined)的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

当尝试访问可能不存在的对象属性时,可选链运算符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链运算符也是很有帮助的。

1
2
3
4
5
6
7
8
9
10
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};

console.log(adventurer.dog?.name); // undefined
console.log(adventurer.someNonExistentMethod?.()); // undefined
console.log(adventurer.someNonExistentMethod()); // Uncaught TypeError: adventurer.someNonExistentMethod is not a function

JavaScript 中的相等性判断

JavaScript 提供三种不同的值比较操作:

  • 严格相等比较 ("strict equality", "identity", "triple equals"),使用 ===。全等操作符比较两个值是否相等,两个被比较的值在比较前都不进行隐式转换。如果两个被比较的值具有不同的类型,这两个值是不全等的。否则,如果两个被比较的值类型相同,值也相同,并且都不是 number 类型时,两个值全等。最后,如果两个值都是 number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0-0 时,两个值被认为是全等的。
  • 抽象相等比较 ("loose equality", "double equals") ,使用 ==。相等操作符比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。在转换后(等式的一边或两边都可能被转换),最终的比较方式等同于全等操作符 === 的比较方式。相等操作符满足交换律。
  • Object.is (ES6),几乎与 === 相同,仅在比较 +0-0NanNaN 时有区别。
x y == === Object.is
undefined undefined true true true
null null true true true
true true true true true
false false true true true
"foo" "foo" true true true
0 0 true true true
+0 -0 true true false
0 false true false false
"" false true false false
"" 0 true false false
"0" 0 true false false
"17" 17 true false false
[1,2] "1,2" true false false
new String("foo") "foo" true false false
null undefined true false false
null false false false false
undefined false false false false
{ foo: "bar" } { foo: "bar" } false false false
new String("foo") new String("foo") false false false
0 null false false false
0 NaN false false false
"foo" NaN false false false
NaN NaN false false true

Promise

同步:顺序执行,执行完一个再执行下一个,需要等待、协调运行。

异步:异步和同步是相对的,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。

注意:

  1. 线程是实现异步的一个方式。可以在主线程创建一个新线程来做某件事,此时主线程不需等待子线程做完而是可以做其他事情。
  2. 异步和多线程并不是一个同等关系。异步是最终目的,多线程只是我们实现异步的一种手段。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise 对象有以下两个特点。

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。

春招笔试题手搓了一个非常绕的题...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const p1 = new Promise(function (resolve, reject) {
setTimeout(function() {
reject(1)
console.log(2)
}, 500)
console.log(3)
})

const p2 = new Promise(function (resolve, reject) {
setTimeout(function() {
resolve(p1)
console.log(4)
}, 1000)
console.log(5)
})

const p3 = new Promise(function (resolve, reject) {
resolve(p2)
console.log(6)
})

p3
.then(result => console.log('p3 Resolved! result = ', result))
.catch(result => console.log('p3 Rejected! result = ', result))

p2
.then(result => console.log('p2 Resolved! result = ', result))
.catch(result => console.log('p2 Rejected! result = ', result))

p1
.then(result => console.log('p1 Resolved! result = ', result))
.catch(result => console.log('p1 Rejected! result = ', result))

console.log(7)

流程:

  1. Promise 新建后会立刻执行,所以分别输出 35
  2. reslove()reject() 会等待该函数执行完毕后再返回结果,所以 6 也会直接输出
  3. 接下来 .then.catch 并没有直接被执行,而只是定义了 Promise 返回后的操作(设置了回调函数),所以直接输出 7
  4. 500ms 后 p1 响应,输出 2,此时 p1 的状态从 pending 转到 rejected
  5. 1000ms 后 p2 响应,输出 4resolve(p1) 表示返回 p1 状态结果,所以也为 rejected
  6. 最后 p3 等到 p2 返回 rejected 而返回 rejected

延伸阅读

参考

  1. 也有说法称 ES6 是一个泛指,指 ES5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017。 ↩︎

探究 JavaScript 语法特性:为什么函数是「一等公民」?
https://ligen.life/2023/javascript-syntax-features/
作者
ligen131
发布于
2023年4月7日
更新于
2023年4月8日
许可协议