前言

本文是通过 廖雪峰的 JavaScript 教程 学习而来. 全文内容几乎与教程无差别, 即本人跟着教程文字和代码均手敲一边, 遂产生此笔记. 笔记文件太大, 遂拆分成多个部分, 这是第二部分, 主要学习了函数, 包括作用域、对象方法、高阶函数、箭头函数、闭包等.

2. 函数

2.1 定义和调用

2.1.1 定义函数

JavaScript 中, 定义函数的方式如下:

1
2
3
4
5
6
function abs(x) {
if (x >= 0) {
return x;
}
return -x;
}

上述 abs() 函数的定义如下:

  • function 是函数定义关键字;
  • abs 是函数的名称( 存在匿名函数没有名称 );
  • (x) 括号内是传入函数的参数, 多个参数用 , 间隔;
  • { ... } 之间的代码是函数体, 可以包含若干语句, 甚至没有语句.

注意 : 函数体内部语句执行时, 一旦执行了 return , 函数就执行完毕, 并将结果返回. 因此, 函数内部通过条件判断和循环可实现非常复杂的逻辑.

如果没有 return 语句, 函数执行完毕后也会返回结果, 只是结果是 undefined.

由于 JavaScript 的函数也是一个对象, 上述 abs() 函数实际上是一个函数对象, 而函数名 abs 可以视为指向该函数的变量.

因此, 第二种定义方式如下:

1
2
3
4
5
6
7
let abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};

这种方式下, function (x) { ... } 是一个匿名函数, 没有函数名. 但是将它赋值给了变量 abs , 所以通过变量 abs 可以调用该函数.

2.1.2 调用函数

调用函数时, 按顺序传入参数:

1
2
abs(10); // 10
abs(-9); // 9

由于 JavaScript 允许传入任意个参数而不影响调用, 因此传入的参数比定义的参数多也没问题, 虽然函数内部并不需要这些参数:

1
2
abs(10, 'balabala'); // 10
abs(-9, 1, 2, 3, null); // 9

传入参数比定义参数的个数少也没有问题:

1
abs(); // 返回 NaN, 因为 参数 x 接收到的是 undefined , -x 无法计算所以为 NaN

检查参数 :

1
2
3
4
5
6
7
8
9
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a Number';
}
if (x >= 0) {
return x;
}
return -x;
}

2.1.3 arguments

JavaScript 还有一个关键字 arguments , 它在函数内部起作用, 并且永远指向当前函数的调用者传入的所有参数. arguments 类似 Array, 但它不是一个 Array:

1
2
3
4
5
6
7
8
9
10
11
function foo(x) {
console.log('x = ' + x);
for (let i = 0; i < arguments.length; i++) {
console.log('arg = ' + arguments[i]);
}
}
foo(10, 20, 30);
// x = 10
// arg = 10
// arg = 20
// arg = 30

利用 arguments 可以获得调用者传入的所有参数, 也就是说, 即便不定义任何参数, 还是可以拿到参数的 :

1
2
3
4
5
6
7
8
9
10
function abs() {
if (arguments.length === 0) {
return 0;
}
let x = arguments[0];
return x >= 0 ? x : -x;
}
abs(); // 0
abs(10); // 10
abs(-10); // -10

实际上 arguments 最常用于判断传入参数的个数, 如下:

1
2
3
4
5
6
7
8
// foo(a[, b], c)
// 接收 2~3 个参数, b 是可选参数, a 和 c 是必选参数, b 默认为 null
function foo(a, b, c) {
if (arguments.length === 2) {
c = b;
b = null;
}
}

要把中间的参数 b 变为可选参数, 就只能通过 arguments 判断, 并重新赋值.

2.1.4 rest 参数

由于 JavaScript 函数允许接收任意个参数, 于是我们就不得不用 arguments 来获取所有参数:

1
2
3
4
5
6
7
8
9
10
11
function foo(a, b) {
let i, rest = [];
if (arguments.length > 2) {
for (i = 2; i < arguments.length; i ++) {
res.push(arguments[i]);
}
}
console.log('a = ', + a);
console.log('b = ', + b);
console.log(rest);
}

为了获得除了已定义的 ab 之外的参数, 我们不得不用 arguments , 并且循环从索引 2 开始以便排除前两个参数, 这种写法特别别扭, 只是为了获得额外的 rest 参数, 有什么更好的办法?

ES6 标准引入了 rest 参数 , 上面的函数可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 []

rest 参数只能写在最后, 前面用 ... 标识, 从运行结果可知, 传入的参数先绑定 ab, 多余的参数以数组形式交给变量 rest, 所以不再需要 arguments 便可以获取全部参数.

如果传入的参数不够正常定义的参数, rest 参数会接收一个空数组而不是 undefined.

2.2 作用域与解构赋值

2.2.1 作用域

JavaScript 中, 通过 var 定义的变量实际上是有作用域的. 如果一个变量在函数体内部声明, 则该变量的作用域为整个函数体, 在函数外不可引用:

1
2
3
4
5
function foo() {
var x = 1;
x = x + 1;
};
x = x + 2; // ReferenceError! 无法在函数体外引用变量 x

不同函数内部的同名变量相互独立, 互不影响:

1
2
3
4
5
6
7
8
function foo() {
var x = 1;
x += 1;
}
function bar() {
var x = 2;
x += 1;
}

嵌套函数中, 内部函数可以访问外部函数定义的变量, 反之不行:

1
2
3
4
5
6
7
function foo() {
var x = 1;
function bar() {
var y = x + 1; // 可以访问
}
var z = y + 1; // ReferenceError! foo 不可以访问 bar 的变量 y!
}

JavaScript 的函数在查找变量时从自身函数定义开始, 从”内”向”外”查找, 如果内部函数定义了与外部函数重名的变量, 则内部变量将 屏蔽 外部变量:

1
2
3
4
5
6
7
8
9
10
function foo() {
var x = 1;
function bar() {
var x = 'A';
console.log('x in bar() = ' + x); // 'A'
}
console.log('x in foo() = ' + x); // 1
bar();
}
foo();

2.2.2 变量提升

JavaScript 的函数定义有个特点, 它会扫描整个函数体的语句, 把所有用 var 声明的变量”提升”到函数顶部:

1
2
3
4
5
6
7
'use strict'
function foo() {
var x = 'Hello, ' + y;
console.log(x);
var y = 'World!';
}
foo(); // 输出 "Hello, undefined"

虽然是 strict 模式, 但语句 var x = 'Hello, ' + y; 并不报错, 因为变量 y 在后面通过 var 声明了, 然而 console.log() 打印 y 的值是 undefined , 这是因为 JavaScript 引擎自动 提升了变量 y 的声明 , 但并 没有提升变量的赋值 , 对于上述 foo() 函数, 实际上执行的是:

1
2
3
4
5
6
7
function foo() {
var y; // 只提升声明, 没有提升赋值
var x = 'Hello, ' + y;
console.log(x);
y = 'World!';
}
foo();

由于 JavaScript 的这一怪异特性, 定义变量时, 必须定义到用到变量的位置之上, 最常见的是在函数内顶部用一个 var 声明函数内部用到的所有变量:

1
2
3
4
5
6
7
8
function foo() {
var
x = 1,
y = x + 1,
z, i;
// 其他语句
...
}

2.2.3 全局作用域

不在任何函数内定义的变量就具有全局作用域, 实际上, JavaScript 默认有一个全局对象 window, 全局作用域的变量实际上是被绑定到 window 的一个属性:

1
2
3
var course = 'Learn JavaScript';
console.log(course); // 'Learn JavaScript'
console.log(window.course); // 'Learn JavaScript'

以变量方式 var foo = function () {} 定义的函数实际上也是一个全局变量, 因此, 顶层函数的定义也被视为一个全局变量, 并绑定到 window 对象:

1
2
3
4
5
function foo() {
alert('foo');
}
foo();
window.foo();

实际上, alert() 函数也是一个 window 的属性:

1
2
3
4
5
6
7
8
9
10
window.alert('调用 window.alert()');
// 把 alert 保存到一个变量
let old_alert = window.alert;
// 给 alert 一个新函数
window.alert = function () {};
alert('无法使用 alert() 显示了!')

// 恢复 alert
window.alert = old_alert;
alert('又可以用 alert() 了');

JavaScript 只有一个全局变量 . 任何变量 ( 包括函数 ) , 如果没有在当前函数作用域中找到, 就会继续向上查找, 如果在全局作用域中也没有找到, 就会报 ReferenceError 错误.

2.2.4 命名空间

全局变量会绑定到 window 上, 不同的 JavaScript 文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数, 都会造成命名冲突, 并且很难发现.

减少冲突的一个办法是, 把自己的所有变量和函数全部绑定到一个全局变量中, 例如:

1
2
3
4
5
6
7
8
// 唯一的全局变量
let MyApp = {};
// 其他变量
MyApp.name = 'MyApp';
MyApp.version = '1.0.1';
MyApp.foo = function () {
return 'foo';
}

把所有的代码全部放入唯一的命名空间 MyApp 中, 会大大减少全局变量冲突. jQuery、YUI、underscore 等 JavaScript 库都是这么做的.

2.2.5 局部作用域

由于 JavaScript 的变量作用域实际上是 函数内部 ( 不是循环内部 ) , 在 for 循环语句中无法定义具有局部作用域的变量:

1
2
3
4
5
6
function foo() {
for (var i = 0; i < 100; i++) {
//
}
i += 100; // 循环以外仍然可以引用变量 i
}

ES6 通过引入 let 关键字解决这一问题, 用 let 替代 var 可以声明一个块级作用域变量:

1
2
3
4
5
6
7
function foo() {
let sum = 0;
for (let i = 0; i < 100; i++) {
sum += i;
}
i += 100; // SyntaxError
}

2.2.6 常量

由于 varlet 声明的是变量, 如果声明一个常量, ES6 之前是不行的, 以前的做法是使用全部大写的变量名自认为其是常量, 不能修改它的值:

1
let PI = 3.1415;

ES6 引入关键字 const 来定义常量, constlet 都具有块级作用域:

1
2
3
const PI = 3.1415;
PI = 3; // 不报错, 但没有效果
PI; // 3.1415

2.2.7 解构赋值

从 ES6 开始, JavaScript 引入了解构赋值, 同时对一组变量进行赋值.

传统方式, 如何把一个数组的元素分别赋值给几个变量:

1
2
3
4
let array = ['hello', 'JavaScript', 'ES5'];
let x = array[0];
let y = array[1];
let z = array[2];

ES6 可以通过解构赋值, 简化过程:

1
2
3
4
5
let [x, y, z] = ['hello', 'JavaScript', 'ES6'];
console.log(`x = ${x}, y = ${y}, z = ${z}`);

// 嵌套也是可以的
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];

解构还可以 忽略某些元素 :

1
2
let [, , z] = ['hello', 'JavaScript', 'ES6'];
z; // ES6

从对象中取出若干属性, 也可以使用解构赋值, 便于快速获取对象的指定属性:

1
2
3
4
5
6
7
let person = {
name: '小明',
age: 20,
gender: 'male'
}
let { name, age } = person;
console.log(`name: ${name}, age: ${age}`);

如果要使使用的 变量名和属性名不一致 , 可以使用 : 方法:

1
2
3
4
5
6
7
8
9
let person = {
name: '小明',
age: 20,
gender: 'male'
}
let { name, age:nianling } = person;
name; // 小明
nianling; // 20
age; // Uncaught ReferenceError: age is not defined

注意 : age 不是变量, 而是对象的属性, nianling 才是变量.

解构赋值还可以使用 默认值 , 这样可以避免属性返回 undefined 的问题:

1
2
3
4
5
6
7
8
9
let person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-123456'
}
let { name, single=true } = person;
name; // 小明
single; // true

当变量已经被声明了, 再次用结构赋值, 将会报错:

1
2
3
4
5
6
let x, y;
{x, y} = { name: '小明', x: 100, y: 200 }; // Uncaught SyntaxError: Unexpected token =

// 这是因为 JavaScript 引擎把 { 开头的语句当作了块处理, 于是 = 不再合法. 解决方法是用小括号包裹:

({x, y} = { name: '小明', x: 100, y: 200 });

2.2.8 使用场景

  1. 交换赋值

    1
    2
    let x = 1, y = 2;
    [x, y] = [y, x]; // 至少在冒泡排序等算法中可以少写两行代码
  2. 快速获取当前页面的域名和路径

    1
    let { hostname:domain, pathname:path } = location;
  3. 函数使用对象作为参数, 解构直接把属性绑定到变量中

    1
    2
    3
    4
    5
    6
    7
    function buildDate({year, month, day, hour=0, minute=0, second=0}) {
    return new Date(`${year}-${month}-${year} ${hour}:${minute}:${second}`);
    }

    // 方便之处是可以只传入 year, month, day 三个参数, 也可以全部传入
    buildDate({ year: 2017, month: 1, day: 1 });
    buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });

2.3 方法

2.3.1 定义方法

JavaScript 中, 在一个对象中绑定函数, 称为这个对象的方法:

1
2
3
4
5
6
7
8
9
10
let xiaoming = {
name: '小明',
birth: 2001,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
}
xiaoming.age; // [Function: age]
xiaoming.age(); // 2024 年 23

绑定到对象上的函数称为方法, 和普通函数没有什么区别, 但是它在内部使用了一个 this 关键字. 在一个方法内部, this 是一个特殊变量, 它始终指向当前对象, 也就是 xiaoming 这个变量. 所以 this.birth 可以拿到 xiaomingbirth 属性.

2.3.2 设计缺陷

拆开写:

1
2
3
4
5
6
7
8
9
10
11
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: '小明',
birth: 2001,
age: getAge
};
xiaoming.age(); // 23
getAge(); // NaN

注意 : 这是 JavaScript 的一个大坑, 单独调用 getAge() 返回 NaN, 以对象方法的形式调用 xiaoming.age() 返回 23. 这是 this 的问题.

当在对象方法中使用 this 时, this 指向被调用的对象, 也就是 xiaoming , 而在函数中使用 this , 比如 getAge() , 此时, this 指向全局对象也就是 window.

如果把 xiaoming.age 赋值给变量, 通过 变量名 调用, 也是不符合预期的:

1
2
let fn = xiaoming.age;
fn; // NaN

如果要使 this 指向正确, 必须使用 obj.func() 的形式调用方法.

注意 : 这是一个巨大的设计缺陷, ECMA 决定在严格模式下, 函数的 this 指向 undefined, 因此, 在严格模式下, 会得到一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

let xiaoming = {
name: '小明',
birth: 2001,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
}
};

let fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined (reading 'birth')

如果你在方法中使用函数, 函数中使用 this ,仍然会报错, 因为这样使用 this 仍然指向 undefined, 在非 strict 模式下指向 window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
},
};

xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined (reading 'birth')

2.3.3 拯救 this

通过一个额外的变量 that 保存 this 的正确指向才可以修复这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';

let xiaoming = {
name: '小明',
birth: 2001,
age: function () {
let that = this; // 在方法内部一开始就把正确的 this 指向捕获保存在 that 中
function getAgeFromBirth() {
let y = new Date().getFullYear();
return y - that.birth;
}
return getAgeFromBirth();
},
};

xiaoming.age(); // 23

2.3.4 apply

我们可以根据是否是 strict 模式, 判断 this 指向的是 undefined 还是 window, 也可以直接控制 this 的指向.

使用函数本身的 apply 方法, 可以指定函数的 this 指向那个对象. 它接收两个参数, 第一个参数就是要绑定的 this 变量, 第二个参数是 Array 表示函数本身的参数:

1
2
3
4
5
6
7
8
9
10
11
function getAge() {
let y = new Date().getFullYear();
return y - this.birth;
}
let xiaoming = {
name: '小明',
birth: 2001,
age: getAge,
};
xiaoming.age();
getAge.apply(xiaoming, []); // 23

2.3.5 call

callapply 唯一的区别是, apply() 把参数打包成 Array 再传入, 而 call() 把参数按顺序传入.

1
2
Math.max.apply(null,[3, 5, 3]); // 5
Math.max.call(null, 3, 5, 2); // 5

对于普通函数, 我们把 this 绑定到 null.

2.3.6 装饰器

利用 apply() , 我们还可以动态改变函数的行为. JavaScript 的所有对象都是动态的, 即使内置的函数, 也可以重新指定新的函数.

假设要统计一下代码调用了 parseInt() 多少次, 可以把所有的调用都找出来, 然后 count += 1, 这样太 low 了, 最佳方法是用我们自己的函数替换掉默认的 parseInt():

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

let count = 0;
let oldParseInt = parseInt;

parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments);
};
parseInt('10');
parseInt('20');
parseInt('30');
console.log(count); // 3

2.4 高阶函数

高阶函数 ( Higher-order Function ). JavaScript 的函数可以指向某个变量, 既然变量可以指向函数, 函数的参数能接受变量, 那么函数就可以把函数作为参数, 这种函数套函数的形式就是高阶函数. 例如:

1
2
3
4
5
6
7
8
function add(x, y, f) {
return f(x) + f(y);
}

// 调用 add(-5, 6, Math.abs) 时
// x, y, f 分别接收了 -5 6 Math.abs
// return 了 Math.abs(-5) + Math.abs(6)
add(-5, 6, Math.abs); // 11

2.4.1 map

要对数组的每一个元素 分别 进行 相同的计算 , 就可以用 map 而不是循环, 例如, 现有函数 f(x) = x^2 , 数组 [1, 2, 3, 4] , 要把数组每一个元素都进行 f(x) 计算, 可以用 map 实现:

1
2
3
4
5
6
function f(x) {
return x * x;
}
let arr = [1, 2, 3, 4];
let res = arr.map(f);
console.log(res); // [1, 4, 9, 16]

map() 作为一个高阶函数, 它把运算规则抽象化了. 不仅仅可以进行简便的计算, 还可以进行复杂的计算, 例如, 把 Array 的所有数字转变为字符串:

1
2
3
let arr = [1, 2, 3, 4];
let res = arr.map(String);
console.log(res); // ['1', '2', '3', '4']

2.4.2 reduce

要对数组的所有元素进行计算, 比如求和, 求阶乘等, 可以看成全部元素做一个计算, 也可以看成每一个元素和下一个元素进行计算再将结果与再下一个元素进行计算直至结束. 这种情况可以使用 reduce . 效果如下:

1
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

例如求阶乘:

1
2
3
4
5
6
7
8
const arr = Array.from({ length: 4 }, (_, i) => i + 1);

function f(x, y) {
return x * y;
}

let res = arr.reduce(f);
console.log(res); // 24 === 1 * 2 * 3 * 4

reduce 可以实现 join 的效果, 例如将 [1, 2, 3, 4] 转变为 1-2-3-4, 方法如下:

1
2
3
4
5
6
7
8
const arr = Array.from({ length: 4 }, (_, i) => i + 1);

function f(x, y) {
return String(x) + '-' + String(y);
}

let res = arr.reduce(f);
console.log(res);

2.4.3 filter

顾名思义, filter 用于过滤掉 Array 的某些元素, 然后返回剩下的元素. 和 map 相似, filter 分别对每一个元素进行相同的计算 ( 判断 ), 然后根据返回值是 true 还是 false 决定保留还是丢弃元素.

例如, 在一个数字 Array 中, 只保留偶数:

1
2
3
4
5
6
7
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let res = arr.filter((item) => {
return item % 2 === 0 ? item : null; // 这种写法多余了
// 采用以下写法即可, 只要返回值是 true 就会保留
return item % 2 === 0;
});
console.log(res); // [2, 4, 6, 8, 10]

2.4.4 回调函数

filter() 接收的回调函数, 可以有多个参数. 通常只取第一个参数( 元素本身 ), 回调函数还有另外两个参数, 表示元素的索引和数组本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr = ['A', 'B', 'C'];
let res = arr.filter(function (element, index, self) {
console.log(element);
console.log(index);
console.log(self);
return true;
});
// 输出
A
0
[ 'A', 'B', 'C' ]
B
1
[ 'A', 'B', 'C' ]
C
2
[ 'A', 'B', 'C' ]

利用 filter() 可以去除 Array 的重复元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let res,
arr = [
'apple',
'banana',
'cherry',
'pear',
'apple',
'orange',
'orange',
'apple',
];
// 由于 indexOf 总是返回元素第一次出现的位置, 后面的重复元素判断时返回值为 false, 不保留
res = arr.filter(function (element, index, self) {
return self.indexOf(element) === index;
});
console.log(res);

2.4.4 sort

JavaScript 的 Array 中的 sort() 方法是用于排序的, 但是排序的结果可能令人奇怪:

1
2
3
4
5
6
7
8
['Google', 'Apple', 'Microsoft'].sort();
// Result: ['Apple', 'Google', 'Microsoft'] 看似正常

['Google', 'apple', 'Microsoft'].sort();
// Result: ['Google', 'Microsoft', 'apple'] apple 排到最后面

[10, 20, 1, 2].sort();
// Result: [1, 10, 2, 20] 莫名其妙?

第二个排序 apple 排到了最后面是因为大写字母的 ASCII 码全部在小写字母的前面, 所以 GM 都在 a 前面.

第三个排序什么情况? 原来是 sort() 默认把所有元素先转换成 String 类型再排序, 结果 10 排在了 2 前面, 因为 1 的 ASCII 码比 2 的小.

实际上 sort() 也是一个高阶函数, 它可以接收一个比较函数实现自定义排序, 按从小到大顺序写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arr = [10, 2, 1, 20];
let res = arr.sort(function (x, y) {
if (x < y) return -200; // 不要返回 true 和 false, 而是正数和负数
if (x > y) return 1231; // 正数, 交换
return 0;
});
console.log(res);

// 倒序
let arr = [10, 2, 1, 20];
let res = arr.sort(function (x, y) {
return y - x; // y > x 时, 把 y 调到前面
});
console.log(res);

默认对字符串排序是区分大小写的, 如果需要忽略大小写, 只需要把字符串全部转化成大写或者小写即可:

1
2
3
4
5
6
7
8
9
10
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res = arr.sort(function (s1, s2) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
if (s1 < s2) return -1;
return 1;
});

console.log(res); // [ 'Amazon', 'apple', 'Facebook', 'Google', 'microsoft' ]

注意 : sort() 方法会直接修改 Array 并返回当前 Array;

1
2
3
4
5
6
7
8
9
10
11
12
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res = arr.sort(function (s1, s2) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
if (s1 < s2) return -1;
return 1;
});

console.log(res); // [ 'Amazon', 'apple', 'Facebook', 'Google', 'microsoft' ]
console.log(arr); // [ 'Amazon', 'apple', 'Facebook', 'Google', 'microsoft' ]
// 所以实际上将 arr.sort() 赋值给 res 是没什么意义的, 原来的 arr 已经丢了

2.4.5 every

every() 方法可以判断数组所有元素是否满足条件, 例如判断字符串数组是不是每个字符都是小写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res1 = arr.every(function (s) {
return s.length > 5;
});

console.log(res1); // false 只有当所有元素的长度都大于 5 时, 才为 true

let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res2 = arr.every(function (s) {
return s.toLowerCase() === s;
});

console.log(res2); // false

2.4.6 find

find() 方法用于查找符合条件的第一个元素, 如果找到了返回这个元素, 否者返回 undefined:

1
2
3
4
5
6
7
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res = arr.find(function (s) {
return s.toLowerCase() === s;
});

console.log(res); // 第一个全小写字符串为 apple

2.4.7 findIndex

findIndex()find() 类似, 也是找到第一个符合规则的元素, findIndex() 返回元素的索引, 找不到返回 -1:

1
2
3
4
5
6
7
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

let res = arr.findIndex(function (s) {
return s.toLowerCase() === s;
});

console.log(res); // 2

2.4.8 forEach

forEach()map() 类似, 它把传入的函数依次作用于每个元素, 但不会返回新数组. forEach() 通常用于遍历数组, 所以传入的函数不需要返回值:

1
2
3
4
5
6
let arr = ['Google', 'Facebook', 'apple', 'Amazon', 'microsoft'];

// 打印每个元素的 toLowerCase 形式
arr.forEach((item) => {
console.log(item.toLowerCase());
});

2.5 闭包

函数除了可以接收一个函数作为参数外, 还可以将函数作为结果返回出去. 例如, 实现一个对 Array 的求和, 一般求和函数如下:

1
2
3
4
5
6
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
})
}
sum([1, 2, 3, 4, 6]); // 15

如果不需要立刻求和, 而是在后面的代码中根据需要再计算怎么办?

可以不返回求和的结果, 而是返回求和的函数:

1
2
3
4
5
6
7
8
function lazy_sum(arr) {
let sum = function () {
return arr.reduce(function (x, y) {
return x + y;
})
}
return sum;
}

当调用 lazy_sum() 时返回的是求和的函数而不是求和结果:

1
2
3
let f = lazy_sum([1, 2, 3, 4, 5]);
console.log(f); // [Function: sum]
f(); // 15

这个例子中, 我们在函数 lazy_sum 中又定义了一个函数 sum , 并且, 内部函数 sum 可以引用外部函数 lazy_sum 的参数和局部变量, 当 lazy_sum 返回函数 sum 时, 相关参数和变量都保存在返回的函数中, 这种称为 “闭包 ( Closure )”的程序结构具有很大的作用.

当调用 lazy_sum 时每一次返回的是一个新的函数, 即使传入相同的参数, 返回的函数也不相同:

1
2
3
let f1 = lazy_sum([1, 2, 3]);
let f2 = lazy_sum([1, 2, 3]);
f1 === f2; // false

f1()f2() 的调用结果也互不影响.

另一个需要注意的问题是, 返回的函数并没有立刻执行,而是直到调用了 f() 才执行.

例子:

1
2
3
4
5
6
7
8
9
10
11
function count() {
let arr = [];
for (var i = 1; i <= 3; i ++) {
arr.push(function () {
return i * i;
})
}
return arr;
}
let results = count();
let [f1, f2, f3] = results;

每次循环生成一个函数存储到 arr 中, 最终返回元素是函数的数组, 被 results 接收, 看上去感觉 f1()f2()f3() 的结果应该是 149, 然而实际结果是:

1
2
3
f1(); // 16
f2(); // 16
f3(); // 16

这是因为返回函数引用了用 var 定义的变量 i , 但它并非立刻执行, 等到三个函数返回时, 它们所引用的变量 i 已经变成了 4, 再调用计算时便全部使用 4 , 得到 16.

注意 : 返回闭包时, 一定 不要引用任何循环变量 , 也不要引用后续会发生变化的变量.

如果一定要引用循环变量, 可以 再创建一个函数 , 用该函数的参数绑定循环变量的当前值, 无论循环变量后续如何更改, 已绑定的函数参数不会变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function count() {
let arr = [];
for (var i = 1; i <= 3; i++) {
arr.push(
(function (n) {
return function () {
return n * n;
};
})(i)
);
}
return arr;
}
let [f1, f2, f3] = count();
console.log(f1()); // 1
console.log(f2()); // 4
console.log(f3()); // 9

这里的 function (x) { return x * x } (3); 是一个立即执行的 匿名函数 , 需要用括号包裹, 否者报错:

1
(function (x) { return x * x } (3));

另一个方法是把循环变量 ilet 声明在 for 循环体中, let 作用域决定了在每次循环时都会绑定新的 i:

1
2
3
4
5
6
7
8
9
10
11
12
13
function count() {
let arr = [];
for (let i = 1; i <= 3; i++) {
arr.push(
(function (n) {
return function () {
return n * n;
};
})(i)
);
}
return arr;
}

如果将 i 定义在 for 循环的外面, 则仍然是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function count() {
let arr = [];
let i;
for (i = 1; i <= 3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
let [f1, f2, f3] = count();
console.log(f1()); // 16
console.log(f2()); // 16
console.log(f3()); // 16

因此 , 最好的办法就是在闭包中不使用后续会发生变化的变量, 否者很难调试.

强大功能 : 在面向对象的设计语言中, 要在对象内部封装一个私有变量, 可以用 private 修饰一个成员变量, 在没有 class 机制, 只有函数的语言里, 借助闭包, 可以封装一个私有变量, 例如, 设计一个计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function create_counter(initial) {
let x = initial || 0;
return {
inc: function () {
x += 1;
return x;
},
};
}

let c1 = create_counter();
console.log(c1.inc()); // 1
console.log(c1.inc()); // 2
console.log(c1.inc()); // 3

在返回的对象中, 实现了一个闭包, 携带了局部变量 x, 且从外部代码是无法访问到 x 的, 也就是说闭包是携带状态的函数, 并且它的状态可以完全隐藏起来.

闭包还可以把多参数的函数转变成单参数的函数. 例如要计算 x^y 可以用 Math.pow(x, y) 函数, 不过考虑到经常计算 x^2x^3 , 我们可以利用闭包创建新的函数 pow2pow3:

1
2
3
4
5
6
7
8
9
10
11
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
};
}

let pow2 = make_pow(2);
let pow3 = make_pow(3);

console.log(pow2(3)); // 9
console.log(pow3(3)); // 27

2.6 箭头函数

2.6.1 定义方式

ES6 标准引入了一种新的函数: 箭头函数 ( Arrow Function ).

箭头函数相当于匿名函数, 并且简化了函数定义. 有以下三种形式

1
2
3
4
5
6
7
8
9
// 三种形式
x => x * x // 省略 `{ ... }` 和 return
() => { return x * x }
(x, y) => { return x * y }

// 如果返回对象不能是下方的写法
x => { foo: x }
// 因为和函数体的 { ... } 有语法冲突, 应该是括号包裹
x => ({ foo: x })

this

箭头函数看上去是一种匿名函数的简写, 但是实际上, 箭头函数和匿名函数有明显的区别, 箭头函数内部的 this 是词法作用域, 由上下文决定, 之前的例子中, 由于 JavaScript 对 this 绑定的错误处理, 下面的例子无法获得预期结果:

1
2
3
4
5
6
7
8
9
10
let obj = {
birth: 2001,
getAge: function () {
let b = this.birth; // 2001
let fn = function () {
return new Date().getFullYear() - this.birth; // this 指向 window 或 undefined
}
return fn();
}
}

现在, 箭头函数完全修复了 this 的指向问题, this 总是指向词法作用域, 也就是外层调用者 obj:

1
2
3
4
5
6
7
8
9
10
let obj = {
birth: 2001,
getAge: function () {
let b = this.birth; // 2001
let fn = () => new Date().getFullYear() - this.birth; // this 指向 obj
}
return fn();
}
}
obj.getAge(); // 23

实操 : 使用箭头函数简化排序传入的函数:

1
2
3
let arr = [10, 20, 1, 2];
arr.sort((x, y) => x - y );
console.log(arr); // [1, 2, 10, 20]

2.7 标签函数

2.8 生成器

3. 标准对象

3.1 Date

3.1.1 获取时间

在 JavaScript 中, Date 对象是表示时间和日期的. 获取系统时间的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
let now = new Date();

console.log(now); // 2024-11-10T07:47:31.460Z
console.log(now.getFullYear()); // 2024
console.log(now.getMonth()); // 10 表示 11 月
console.log(now.getDate()); // 10 表示 10 号
console.log(now.getDay()); // 0 表示 周日
console.log(now.getHours()); // 15 24小时制
console.log(now.getMinutes()); // 48 分钟
console.log(now.getSeconds()); // 47 秒
console.log(now.getMilliseconds()); // 991 微秒
console.log(now.getTime()); // 时间戳 number 形式

3.1.2 创建 Date

如果要创建一个指定日期和时间的 Date 对象:

1
2
let d = new Date(2015, 5, 19, 20, 15, 30, 123);
console.log(d); // 2015-06-19T12:15:30.123Z

注意 : JavaScript 作者脑抽, Date 对象 Month 是从 0 开始的, 即 1-12月对应 Month 0-11.

另一种创建方法是解析一个符合 ISO 8601 格式的字符串:

1
2
let d = Date.parse('2015-06-24T19:49:22.875+08:00');
console.log(d); // 1435146562875

时区 , Date 对象总是按浏览器所在时区显示的, 可以调整时区:

1
2
3
llet d = new Date(1435146562875);
console.log(d.toLocaleString()); // 6/24/2015, 7:49:22 PM
console.log(d.toUTCString()); // Wed, 24 Jun 2015 11:49:22 GMT

3.2 RegExp

3.2.1 正则表达式

正则表达式是用来匹配字符串的强大工具, 他的思想是用一种描述性的语言给字符串定义一个规则, 凡是符合规则的字符串, 认为匹配成功, 否则就是不合法的.

正则表达式本身也是一个字符串, 在正则表达式中如果直接给出字符, 就是精确匹配, 用 \d 可以匹配一个数字, \w 可以匹配一个字母或者数字, . 可以匹配任意字符, \s 匹配一个空格,

要匹配长度不定的字符串, 在正则表达式中, 用 * 表示任意个字符 ( 包括 0 个 ), 用 + 表示至少一个字符, 用 ? 表示 01 个字符, 用 {n} 表示 n 个字符, 用 {n, m} 表示 n - m 个字符.

例如要匹配手机号, 手机号的规则是 11 位, 第二位从 3 到 9:

1
'1[3-9]\d{9}'

如果要匹配带区号的电话号, 三位区号加 3 到 8 位号码:

1
'\d{3}\-\d{3, 8}'

3.2.2 进阶

要更精确的匹配, 需要用到 [] 表示范围, | 表示或者, ^ 表示行的开头, $ 表示行的结束, 例如:

  • [0-9a-zA-Z\_] 表示匹配一个数字、字母或者下划线 ( 表达式[] 内的 \ 用作转义 )
  • [0-9a-zA-Z\_]+ 表示至少匹配一个数字、字母或者下划线, 比如 a1000_Z
  • [a-zA-z\_\$][0-9a-zA-Z\_\$]* 表示匹配由字母或下划线开头, 后面接任意个由一个数字、字母或者下划线组成的字符串, 也就是 JavaScript 允许的变量名

3.2.3 RegExp

JavaScript 中有两种创建正则表达式的方法, 第一种是直接通过 /正则表达式/ 写出来, 第二种是通过 new RegExp('正则表达式') 创建一个 RegExp 对象.

1
2
3
4
let re1 = /ABC\-001/;
let re2 = new RegExp('ABC\\-001'); 这里必须是两个 \ , 转义出来一个 \ 才是正则表达式的内容, 如果只用一个, re2 将是 /ABC-001/
console.log(re1); // /ABC\-001/
console.log(re2); // /ABC\-001/

尝试 :

1
2
3
4
let re = /^\d{3}\-\d{3,8}$/;
console.log(re.test('010-12345')); // true
console.log(re.test('010-1234x')); // false
console.log(re.test('010 12345')); // false

切分字符串

常规 str.split(' ') 切分字符串无法识别连续的空格, 通过正则表达式可以灵活的切分字符串:

1
2
3
4
5
'a b  c'.split(' '); // ['a', 'b', '', 'c'] 第三个元素是空 ''
'a b c'.split(/\s+/); // ['a', 'b', 'c']
// 字符串还有 "杂质字符时"
'a, b, c'.split(/[\s+\,]+/) // ['a', 'b', 'c'] `[]` 外面有 + 里面其实可以省略
'a, b;; c d'.split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']

分组

除了单纯的匹配以外, 正则表达式还有提取子串的强大功能, 用 () 表示就是要提取的分组 ( Group ), 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let re = /^(\d{3})-(\d{3,8})$/;
console.log(re.exec('010-12345'));
console.log(re.exec('010 12345'));

// 输出
[
'010-12345',
'010',
'12345',
index: 0,
input: '010-12345',
groups: undefined
]
null

如果正则表达式中定义了组, 就可以在 RegExp 对象上用 exec() 方法提取出子串来. exec() 方法在匹配成功后, 会返回一个 Array, 第一个元素是正则表达式匹配到的整个字符串, 后面的字符串表示匹配成功的子串. 失败时返回 null.

贪婪匹配

正则表达式默认是贪婪匹配, 就是尽可能匹配更多的字符. 例如, 匹配出数字后面的 0:

1
2
let re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']

由于 \d+ 采用贪婪匹配, 直接把 0 也匹配上了, 所以 0* 匹配到了空字符串. 必须让 \d+ 采用非贪婪匹配 ( 也即是尽可能匹配更少的字符 ), 才能把后面的 0 匹配出来, 加个 ? 就可以让 \d+ 采用非贪婪匹配:

1
2
let re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']

全局搜索

JavaScript的正则表达式还有一些特殊标志, 最常用的是 g, 表示全局匹配:

1
2
3
let r1 = /test/g;
// 等同于
let r2 = new RegExp('test', 'g');

全局匹配多次进行 exec() 方法来搜索一个匹配的字符串, 当我们指定 g 标志后, 每次运行 exec() , 正则表达式本身会更新 lastIndex 属性, 表示上一次匹配到的最后的索引:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let s = 'JavaScript, VBScript, JScript and ECMAScript';
let re = /[a-zA-Z]+Script/g;

// 使用全局匹配:
re.exec(s); // ['JavaScript']
console.log(re.lastIndex); // 10, 10 的位置是下一次匹配的位置, 也就是 ',' 的位置

re.exec(s); // ['VBScript']
console.log(re.lastIndex); // 20 是 ',' 的位置

re.exec(s); // ['JScript']
console.log(re.lastIndex); // 29 是 ' ' 的位置

re.exec(s); // ['ECMAScript']
console.log(re.lastIndex); // 44 是 undefined

re.exec(s); // null,直到结束仍没有匹配到
console.log(re.lastIndex); // 0

尝试 : 是不是可以写成循环呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let s = 'JavaScript, VBScript, JScript and ECMAScript';
let re = /[a-zA-Z]+Script/g;

do {
re.exec(s);
console.log(re.lastIndex);
} while (re.lastIndex !== 0);

// 输出 效果一致
10
20
29
44
0

其他标志

i 标志表示忽略大小写, m 标志表示执行多行匹配.

3.3 JSON

3.3.1 介绍

JSON 是 JavaScript Object Notation 的缩写, 它是一种数据交换格式.

在 JSON 出来之前, 一直是用 XML 来传递数据, 因为 XML 是一种纯文本格式, 所以适合在网络上交换数据. XML 本身不复杂, 但是加上 DTD、XSD、Xpath、XSLT 等一大堆复杂的规范后, 变得让人头大. 花在弄懂规范上的时间剧增.

2002年, 聪明的道格拉斯·克洛克福特发明了 JSON 这一数据交换格式. JSON 实际上是 JavaScript 的一个子集, 在 JSON 中, 一共就只有以下几种数据类型:

  • number : 和 JavaScript 的 number 完全一致
  • boolean : 就是 JavaScript 的 truefalse
  • string : 就是 JavaScript 的 string
  • null : 就是 JavaScript 的 null
  • array : 就是 JavaScript 的 Array 表示方式: []
  • object 就是 JavaScript 的 { ... }

JSON 定死了字符集必须是 UTF-8, 表示多语言没有问题. 为了统一解析, JSON 的字符串必须使用 "" , Object 的键也必须使用 ""

JSON 使用简单, 很快成为 ECMA 的标准. JavaScript 中可以直接使用 JSON, 因为内置了 JSON 的解析.

把任何 JavaScript 对象变成 JSON, 就是把这个对象序列化成一个 JSON 格式的字符串, 这样才能进行通信.

如果收到一个 JSON 字符串, 只需要对其反序列化成一个 JavaScript 对象, 就可以直接使用这个对象了.

3.3.2 序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let xiaoming = {
name: '小明',
age: 23,
gender: true,
height: 1.80,
grade: null,
'middle-school': '\"W3C\" Middle School',
skills: [
'JavaScript', 'Java', 'Python', 'Lisp'
]
};
let s = JSON.stringify(xiaoming);
console.log(s);
// 输出 : {"name":"小明","age":23,"gender":true,"height":1.8,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}

要美化输出, 可以加上参数, 按缩进输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let xiaoming = {
name: '小明',
age: 23,
gender: true,
height: 1.8,
grade: null,
'middle-school': '"W3C" Middle School',
skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
};
// 第一个参数是要序列化的对象
// 第二个参数是控制如何筛选对象的键值
// 第三个选项是缩进符号
let s = JSON.stringify(xiaoming, ['name', 'skills'], ' '); // 输出 name 和 skills
console.log(s);

还可以传入一个函数, 对象的每个键值对都会被函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let xiaoming = {
name: '小明',
age: 23,
gender: true,
height: 1.8,
grade: null,
'middle-school': '"W3C" Middle School',
skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
};

function convert(key, value) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value;
}
// 键值对, 值为 string 类型时, 改为全大写
let s = JSON.stringify(xiaoming, convert, ' ');
console.log(s);

如果需要精确控制序列化哪些项, 给 xiaoming 定义一个 toJSON() 方法, 直接返回 JSON 应该序列化的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let xiaoming = {
name: '小明',
age: 23,
gender: true,
height: 1.8,
grade: null,
'middle-school': '"W3C" Middle School',
skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
toJSON: function () {
return {
Name: this.name,
Age: this.age,
Skills: this.skills,
};
},
};
let s = JSON.stringify(xiaoming);
console.log(s);
// {"Name":"小明","Age":23,"Skills":["JavaScript","Java","Python","Lisp"]}

3.3.3 反序列化

接收到一个 JSON 格式字符串, 用 JSON.parse() 可以把它变成一个 JavaScript 对象:

1
2
3
let x = JSON.parse('{"name": "John", "age": 30, "city": "Suzhou"}');
console.log(x);
// { name: 'John', age: 30, city: 'Suzhou' }

JSON.parse() 可以接收一个函数, 用来转换解析出的属性:

1
2
3
4
5
6
7
8
9
10
11
let obj = JSON.parse(
'{"name": "John", "age": 16, "city": "Suzhou"}',
(key, value) => {
if (key === 'name') {
return value + '同学';
}
return value;
}
);
console.log(obj);
// { name: 'John同学', age: 16, city: 'Suzhou' }

第二部分结束