什么是JavaScript
JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名,但是它也被用到了很多非浏览器环境中,JavaScript 基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。
JavaScript在1995年由Netscape公司的Brendan Eich,在网景导航者浏览器上首次设计实现而成。因为Netscape与Sun合作,Netscape管理层希望它外观看起来像Java,因此取名为JavaScript。但实际上它的语法风格与Self及Scheme较为接近。
JavaScript 的标准是 ECMAScript 。截至 2012 年,所有的现代浏览器都完整的支持 ECMAScript 5.1,旧版本的浏览器至少支持 ECMAScript 3 标准。2015年6月17日,ECMA国际组织发布了 ECMAScript 的第六版,该版本正式名称为 ECMAScript 2015,但通常被称为 ECMAScript 6 或者 ES6。自此,ECMAScript 每年发布一次新标准。本文档目前覆盖了最新 ECMAScript 的草案,也就是 ECMAScript2020
发展历程
JavaScript最初由Netscape的Brendan Eich设计,最初将其脚本语言命名为LiveScript,后来Netscape在与Sun合作之后将其改名为JavaScript。JavaScript最初受Java启发而开始设计的,目的之一就是“看上去像Java”,因此语法上有类似之处,一些名称和命名规范也借自Java,但JavaScript的主要设计原则源自Self和Scheme。JavaScript与Java名称上的近似,是当时Netscape为了营销考虑与Sun微系统达成协议的结果。微软同时期也推出了JScript来迎战JavaScript的脚本语言。
发展初期,JavaScript的标准并未确定,同期有Netscape的JavaScript,微软的JScript和CEnvi的ScriptEase三足鼎立。为了互用性,Ecma国际(前身为欧洲计算机制造商协会)创建了ECMA-262标准(ECMAScript),两者都属于ECMAScript的实现,尽管JavaScript作为给非程序人员的脚本语言,而非作为给程序人员的脚本语言来推广和宣传,但是JavaScript具有非常丰富的特性。 1997年,在ECMA(欧洲计算机制造商协会)的协调下,由Netscape、Sun、微软、Borland组成的工作组确定统一标准:ECMA-262。完整的JavaScript实现包含三个部分:ECMAScript,文档对象模型,浏览器对象模型。
JavaScript是甲骨文公司的注册商标。Ecma国际以JavaScript为基础制定了ECMAScript标准。JavaScript也可以用于其他场合,如服务器端编程(Node.js)。
主要功能
- 嵌入动态文本于HTML页面。
- 对浏览器事件做出响应。
- 读写HTML元素。
- 在数据被提交到服务器之前验证数据。
- 检测访客的浏览器信息。 控制cookies,包括创建和修改等。
- 基于Node.js技术进行服务器端编程。
语言组成
- ECMAScript,描述了该语言的语法和基本对象。
- 文档对象模型(DOM),描述处理网页内容的方法和接口。
- 浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口。
DOM和BOM
- DOM 是 W3C 的标准,document就是dom对象; [所有浏览器公共遵守的标准]
- BOM 是 各个浏览器厂商根据 DOM在各自浏览器上的实现;[表现为不同浏览器定义有差别,实现方式不同]
- window 是 BOM 对象,而非 js 对象;
基本语法规范
- JavaScript并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;。
让JavaScript引擎自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致。 所以为了避免这样的状况出现,每句结尾都要加上;。
if (2 > 1) {
x = 1;
y = 2;
z = 3;
}
- 缩进要求是为了代码整洁度以及阅读方便。
- 注释://行注释 /* */块注释
- JavaScript严格区分大小写,如果弄错了大小写,程序将报错或者运行不正常。
数据类型和数据结构(ECMAScript)
1.动态类型
JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据:
// java 用java举例
var test = new String();
test = "测试";
// javascrpit
var test = "测试"
test = 42;
2.数据类型
最新的 ECMAScript 标准定义了 9 种数据类型:
- 6 种原始类型,使用 typeof 运算符检查:
- undefined:typeof(undefined) === "undefined"
- Boolean:typeof (true) === "boolean"
- Number:typeof (1) === "number"
- String:typeof ("测试") === "string
- BigInt:typeof (123872728n) === "bigint"
- Symbol :typeof (Symbol()) === "symbol"
- null:typeof () === "object"。
- Object:typeof () === "object"。任何 constructed 对象实例的特殊非数据结构类型,也用做数据结构:new Object,new Array,new Map,new Set,new WeakMap,new WeakSet,new Date,和几乎所有通过 new keyword 创建的东西。
- Function:非数据结构,尽管 typeof 操作的结果是:typeof () === "function"。这个结果是为 Function 的一个特殊缩写,尽管每个 Function 构造器都由 Object 构造器派生。
Number
你可以在变量中存储数字,不论这些数字是像30(也叫整数)这样,或者像2.456这样的小数(也叫做浮点数)。当你给一个变量数字赋值时,不需要用引号括起来。
Boolean
Boolean 的值有2种:true或false。它们通常被用于在适当的代码之后,测试条件是否成立。
String
字符串是文本的一部分。当你给一个变量赋值为字符串时,你需要用单引号或者双引号把值给包起来,否则JavaScript将会把这个字符串值理解成别的变量名。
BigInt
BigInt类型是 JavaScript 中的一个基础的数值类型,可以用任意精度表示整数。使用 BigInt,您可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。BigInt是通过在整数末尾附加 n 或调用构造函数来创建的。
Symbol
符号(Symbols)是ECMAScript 第6版新定义的。符号类型是唯一的并且是不可修改的, 并且也可以用来作为Object的key的值;
console.log(Symbol('1') !== Symbol('1')) // true
// 可以用作属性判断
var race = {
protoss: Symbol(),
terran: Symbol(),
zerg: Symbol()
}
race.protoss !== race.terran // true
race.protoss !== race.zerg // true
常见操作方法
1.常见字符串操作
var s = 'Hello, world!';
s.length; // 13
s[0]; // 'H'
s[6]; // ' '
s[7]; // 'w'
s[12]; // '!'
s[13]; // undefined 超出范围的索引不会报错,但一律返回undefined
toUpperCase
字符串大写
var s = 'Hello';
s.toUpperCase(); // 返回'HELLO'
toLowerCase
字符串小写
var s = 'Hello';
s.toLowerCase(); // 'hello'
indexOf
搜索指定字符串出现的位置
var s = 'hello, world';
s.indexOf('world'); // 返回7
s.indexOf('World'); // 没有找到指定的子串,返回-1
substring
返回指定索引区间的子串
var s = 'hello, world'
s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello'
s.substring(7); // 从索引7开始到结束,返回'world'
split
根据条件将字符串分成对应的元素,返回一个数组
let str = 'admin;123456'
console.log(str.split(';'))
// ['admin','123456']
2.常见数组操作
indexOf
与String类似,Array也可以通过indexOf()来搜索一个指定的元素的位置
var arr = [10, 20, '30', 'xyz'];
arr.indexOf(10); // 元素10的索引为0
arr.indexOf(20); // 元素20的索引为1
arr.indexOf(30); // 元素30没有找到,返回-1
arr.indexOf('30'); // 元素'30'的索引为2
slice
slice()就是对应String的substring()版本,它截取Array的部分元素,然后返回一个新的Array
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
arr.slice(0, 3); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C']
arr.slice(3); // 从索引3开始到结束: ['D', 'E', 'F', 'G']
如果不给slice()传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个Array
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
var aCopy = arr.slice();
aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G']
aCopy === arr; // false
push和pop
push()向Array的末尾添加若干元素,pop()则把Array的最后一个元素删除掉
var arr = [1, 2];
arr.push('A', 'B'); // 返回Array新的长度: 4
arr; // [1, 2, 'A', 'B']
arr.pop(); // pop()返回'B'
arr; // [1, 2, 'A']
arr.pop(); arr.pop(); arr.pop(); // 连续pop 3次
arr; // []
arr.pop(); // 空数组继续pop不会报错,而是返回undefined
arr; // []
unshift和shift
var arr = [1, 2];
arr.unshift('A', 'B'); // 返回Array新的长度: 4
arr; // ['A', 'B', 1, 2]
arr.shift(); // 'A'
arr; // ['B', 1, 2]
arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次
arr; // []
arr.shift(); // 空数组继续shift不会报错,而是返回undefined
arr; // []
sort
sort()可以对当前Array进行排序,它会直接修改当前Array的元素位置,直接调用时,按照默认顺序排序
var arr = ['B', 'C', 'A'];
arr.sort();
arr; // ['A', 'B', 'C']
reverse
reverse()把整个Array的元素给调个个,也就是反转
var arr = ['one', 'two', 'three'];
arr.reverse();
arr; // ['three', 'two', 'one']
splice
splice()方法是修改Array的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
concat
concat()方法把当前的Array和另一个Array连接起来,并返回一个新的Array
var arr = ['A', 'B', 'C'];
var added = arr.concat([1, 2, 3]);
added; // ['A', 'B', 'C', 1, 2, 3]
arr; // ['A', 'B', 'C']
实际上,concat()方法可以接收任意个元素和Array,并且自动把Array拆开,然后全部添加到新的Array里
var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'
join
join()方法是一个非常实用的方法,它把当前Array的每个元素都用指定的字符串连接起来,然后返回连接后的字符串
var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'
数组总结
// 改变原数组的方法
push() // 返回数组长度
pop() // 返回删除元素
unshift() // 返回数组长度
shift() // 返回删除元素
sort()
reverse()
splice() // 返回删除元素
// 没有改变原数组
indexOf() // 返回新数组
slice() // 返回新数组
concat() // 返回新数组
join() // 返回字符串
函数
1.函数定义及调用
定义函数的两种方式
- 第一种
// function指出这是一个函数定义;
// abs是函数的名称;
// (x)括号内列出函数的参数,多个参数以,分隔;
// { ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
- 第二种
// function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了
变量abs,所以,通过变量abs就可以调用该函数
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
- 调用
abs(10); // 返回10
abs(-9); // 返回9
abs(); // 返回NaN; abs(x)函数的参数x将收到undefined,计算结果为NaN
扩展字段
- arguments 只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) {
if (arguments.length === 2) {
// 实际拿到的参数是a和b,c为undefined
c = b; // 把b赋给c
b = null; // b变为默认值
}
}
- rest 预防传入额外的所有参数
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 结果:
// a = 1
// b = undefined
// Array []
2.作用域
es5 直有全局作用域,es6才有区分
块级作用域
let实际上为JavaScript新增了块级作用域
function block() {
let n = 5;
if(true){
let n = 10;
}
console.log(n); // 5
}
function block() {
var n = 5;
if(true){
let n = 10;
}
console.log(n); // 5
}
function block() {
var n = 5;
if(true){
var n = 10;
}
console.log(n); //10
}
function block() {
let n = 5;
if(true){
var n = 10;
}
console.log(n); // 报错
}
上面的函数有2个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了
// IIFE写法
(function(){
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
局部作用域
整个函数体,由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的
function foo() {
var x = 1;
x = x + 1;
}
x = x + 2; // ReferenceError! 无法在函数体外引用变量x
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // ReferenceError: i is not defined
*******************************************
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
*******************************************
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响
function foo() {
var x = 1;
x = x + 1;
}
function bar() {
var x = 'A';
x = x + 'B';
}
由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以访问foo的变量x!
}
var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}
全局作用域
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
3.解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
// 正常写法
let a = 1;
let b = 2;
let c = 3;
// es6允许
let [a, b, c] = [1, 2, 3];
// “模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
// 不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
数组解构-默认值
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
// 最后一个表达式之所以会报错,是因为x用y做默认值时,y还没有声明。
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
// 等号右边的对象没有baz属性,所以变量foo取不到值,所以等于undefined。
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
// 例一
let { log, sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello
上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。
如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
对象的解构赋值是下面形式的简写
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
对象变量解构赋值注意点
与数组一样,解构也可以用于嵌套结构的对象。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
下面是嵌套赋值的例子
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
let {foo: {bar}} = {baz: 'baz'};
上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。
注意,对象的解构赋值可以取到继承的属性。
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"
上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。
对象解构-默认值
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
对象解构-注意点
(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式,该表达式虽然毫无意义,但是语法是合法的,可以执行。
({} = [true, false]);
({} = 'abc');
({} = []);
(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。
// 例子
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值。
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。
undefined就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]