0、一些链接

1、快速入门阶段

https://liaoxuefeng.com/books/javascript/quick-start/index.html

1.1 循环

  • for循环里面需要用 ;,而非,
  • for循环对于array的循环,一般用< arr.length而非<= arr.length,因为length不存在0,但是index存在0(与python一样);
let arr = ['Apple', 'Google', 'Microsoft'];
let i, x;
for (i=0; i<arr.length; i++) {
    x = arr[i];
    console.log(x);
}

1.2 对象

  • 对象里面需要用,隔开各个属性,尾部用;结尾;
let xiaohong = {
    name: '小红',
    'middle-school': 'No.1 Middle School'
};

1.3 Map和Set、Iterable

1.3.1 Map

  • Map可以用来快速使用两个array创建一个object
let names = ['Michael', 'Bob', 'Tracy'];
let scores = [95, 75, 85];

// 方法零
let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

// 方法一
let m = new Map();
for (let i = 0; i < names.length; i++) {
    m.set(names[i], scores[i]);
}

// 方法二
let m = new Map(names.map((name, index) => [name, scores[index]]));

console.log(m.get('Michael')); // 输出 95
console.log(m); // 打印完整Map(可选)

方法二讲解

names.map() = 遍历 names 数组里的每一个名字,循环处理,并拿到index 012 它会自动循环 3 次(因为 names 有 3 个元素)

names.map((name, index) => [name, scores[index]])

会自动生成一个二维数组(正好是 Map 需要的格式):

[
  ['Michael', 95],
  ['Bob', 75],
  ['Tracy', 85]
]

1.3.2 Set

  • Set类似python元组,自动过滤重复元素
let s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}
  • 遍历Array可以采用下标循环,遍历MapSet就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。
let a = ['A', 'B', 'C'];
let s = new Set(['A', 'B', 'C']);
let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (let x of a) { // 遍历Array
    console.log(x);
}
for (let x of s) { // 遍历Set
    console.log(x);
}
for (let x of m) { // 遍历Map
    console.log(x[0] + '=' + x[1]);
}
  • for ... of循环它只循环集合本身的元素:
let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x of a) {
    console.log(x); // 'A', 'B', 'C'
}

1.3.3 forEach

  • 更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数
let a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
    // element: 指向当前元素的值
    // index: 指向当前索引
    // array: 指向Array对象本身
    console.log(`${element}, index = ${index}`);
});
A, index = 0
B, index = 1
C, index = 2
// Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
let s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});

// Map的回调函数参数依次为value、key和map本身:
let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
    console.log(value);
});

2、函数

JavaScript函数允许接收任意个参数,而且只会用到<= arguments.length个数的参数,如下面的abs(10,-100)只会用到10,二忽视-100,剩下的这些不要的参数我们放到了rest里面去。

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

2.1 arguments

JavaScript里面的函数,自动带一个参数arguments,且永远指向当前函数的调用者传入的所有参数,这也说明了即使函数不定义任何参数,还是可以拿到参数的值。

function foo(x) {
    if (arguments.length === 0) {
        return 0;
    } else {
    console.log('x = ' + x); // 10
    }
    for (let i=0; i<arguments.length; i++) {
        console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
    }
}
foo(10, 20, 30);
foo();
x = 10
arg 0 = 10
arg 1 = 20
arg 2 = 30

而且在ES6中,可以用...rest来指代这些被忽略的剩余参数,因为他们也有未竟的作用,注意在使用的时候应该用rest而非...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 ]

2.2 return的坑

JavaScript引擎有一个在行末自动添加分号的机制,这可能让你栽到return语句的一个大坑:

function foo() {
    return { name: 'foo' };
}

foo(); // { name: 'foo' }

如果把return语句拆成两行:

function foo() {
    return
        { name: 'foo' };
}

foo(); // undefined

要小心了,由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了:

function foo() {
    return; // 自动添加了分号,相当于return undefined;
        { name: 'foo' }; // 这行语句已经没法执行到了
}

所以正确的多行写法是:

function foo() {
    return { // 这里不会自动加分号,因为{表示语句尚未结束
        name: 'foo'
    };
}

2.3 变量提升

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

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}

foo();
// Hello, undefined
  • 虽然是strict模式,但语句var x = 'Hello, ' + y;并不报错,原因是变量y在稍后申明了。但是console.log显示Hello, undefined,说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。

  • 如果后续不声明y,会报错。

  • 由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量。

2.4 全局变量

全局变量都绑定在window身上的。

function foo() {
    alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

2.5 局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的,但是改成ES6中的let就可以做到,for循环里面的let变量在外部无法访问。

function foo() {
    for (var i=0; i<100; i++) {
    // for (let i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

2.6 解构赋值

2.6.1 array

let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

2.6.2 对象

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

let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};
let {name, age, passport} = person;

// name, age, passport分别被赋值为对应属性:
console.log(`name = ${name}, age = ${age}, passport = ${passport}`);
// name = 小明, age = 20, passport = G-12345678

果要使用的变量名和属性名不一致,可以用下面的语法获取

let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined

如果person对象没有single属性,默认赋值为true

let {name, single=true} = person;

有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误

// 声明变量:
let x, y;
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =

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

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

2.7 this? that?

2.7.1 古老方法

在一个方法内部,this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaomingbirth属性。

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

console.log(xiaoming.age); // function xiaoming.age()
console.log(xiaoming.age()); // 今年调用是36,明年调用就变成37了

// 但是直接用getAge会爆粗,因为他的this没来

如果对象内部属性指向一个函数,且函数内部使用了this,此时如果从外部去获取这个对象里面的这个属性(函数),会报错,因为那个函数里面使用的this指向那个对象,你在外部调用就指向了window了!

而且,对象属性里面函数如果嵌套了一个函数,嵌套函数里面用this是拿不到这个对象的,而是指向window,除非提前用that捕获这个对象,然后通过内部函数可以访问外部变量,外部函数不能访问内部变量的特性,从而让嵌套函数内的this重新指向该对象,即that

'use strict';

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        let that = this; // 在方法内部一开始就捕获this
        function getAgeFromBirth() {
            let y = new Date().getFullYear();
            return y - that.birth; // 用that而不是this
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // 25

2.7.2 apply

可以用apply改变this的指向对象,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。

function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
}

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

console.log(xiaoming.age()); // 25
console.log(getAge.apply(xiaoming, [])); // 25, this指向xiaoming, 参数为空

2.7.3 call

类似apply,区别在于:

  • apply()把参数打包成Array再传入;
  • call()把参数按顺序传入。

比如调用Math.max(3, 5, 4),分别用apply()call()实现如下:

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

对普通函数调用,我们通常把this绑定为null

2.7.4 装饰器

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。

'use strict';

let count = 0;
let oldParseInt = parseInt; // 保存原函数

window.parseInt = function () {
    count += 1;
    return oldParseInt.apply(null, arguments); // 调用原函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3

2.8 高阶函数

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

2.8.1 map/reduce

如果你读过Google的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白map/reduce的概念。

MapReduce 就是: 把超级大的数据,先拆开来并行算(Map),再把结果汇总到一起(Reduce)

假如你有 100 亿行日志,要统计:

  • 每个单词出现多少次
  • 每个IP访问了多少次
  • 每个商品卖了多少

但问题来了:

  • 数据怎么切分?
  • 机器之间怎么传数据?
  • 某台机器挂了怎么办?
  • 结果怎么合到一起?

经典例子:统计单词次数(WordCount)

Map(映射)

把一大段文本,拆成很多小片段,丢给很多机器并行处理。每台机器做一件简单的事,遇到一个词,就输出 (单词, 1),比如一句话:"I love JS I love MapReduce",map 之后输出:

(I, 1)
(love, 1)
(JS, 1)
(I, 1)
(love, 1)
(MapReduce, 1)

Shuffle(洗牌,系统自动做)

系统自动把相同 key 的数据,分到同一台机器。比如所有 I 放一起,所有 love 放一起:

I: [1, 1]
love: [1, 1]
JS: [1]
MapReduce: [1]

Reduce(归约)

每台机器拿到一组相同 key 的数据,做聚合,把一堆 1 加起来。

I: 1+1 = 2
love: 1+1 = 2
JS: 1
MapReduce: 1
  • JS array.map():逐个处理元素,输出新数组 → 对应分布式 Map

  • JS array.reduce():把一堆值聚合成一个结果 → 对应分布式 Reduce

Google 那篇论文,就是把这个简单思路,放大到几千台机器的大数据场景

![image-20260406214608194](./images/廖雪峰 JavaScript 学习笔记 —— 初级注意事项/image-20260406214608194.png)

let arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
    return x * 10 + y;
}); // 13579

一个作业:字符串转数字

function string2int(s) {
    let res = [];
    return s.split("").reduce(function(first, second) {
        return first * 10 + second * 1;
    }, 0);
}

s="12345";
console.log(string2int(s));
// 12345

// 测试:
if (string2int('0') === 0 && string2int('12345') === 12345 && string2int('12300') === 12300) {
    if (string2int.toString().indexOf('parseInt') !== -1) {
        console.log('请勿使用parseInt()!');
    } else if (string2int.toString().indexOf('Number') !== -1) {
        console.log('请勿使用Number()!');
    } else {
        console.log('测试通过!');
    }
}
else {
    console.log('测试失败!');
}

2.9 map与parseInt

2.9.1. 错误代码

let arr = ['1', '2', '3'];
let r;
r = arr.map(parseInt);
console.log(r);
[1, NaN, NaN]

2.9.2 核心错误原因分析

2.9.2.1 map 回调函数的传参规则

map 遍历数组时,会自动向回调函数传递3个参数

  1. 当前元素的值
  2. 当前元素的索引
  3. 原数组本身
2.9.2.2 parseInt 函数的参数规则

parseInt 是原生转换函数,接收两个必填参数

  1. 待转换的字符串
  2. 基数(进制数,如10代表十进制、2代表二进制)
2.9.2.3 逐次执行过程拆解

arr.map(parseInt) 等价于把索引当成进制传给 parseInt,执行流程:

  1. parseInt('1', 0):基数0默认按十进制解析 → 结果 1
  2. parseInt('2', 1):基数1为无效进制 → 结果 NaN
  3. parseInt('3', 2):基数2(二进制),但3不是有效二进制数 → 结果 NaN

2.9.3 正确使用方法

1. 显式指定十进制调用 parseInt

通过箭头函数包裹,固定进制为10,屏蔽索引干扰:

let arr = ['1', '2', '3'];
let r = arr.map(x => parseInt(x, 10));
console.log(r); // [1, 2, 3]

2. 直接使用 Number 函数(推荐)

Number 函数只接收1个参数,不受 map 索引影响,更简洁:

let arr = ['1', '2', '3'];
let r = arr.map(Number);
console.log(r); // [1, 2, 3]
  1. 禁止直接用 arr.map(parseInt),会因索引被当作进制导致转换失败
  2. parseInt 必须通过箭头函数手动传参+指定十进制
  3. 简单字符串转数字,优先用 arr.map(Number),无参数陷阱

2.10 filter

filter只能在数组上用

map()类似,Arrayfilter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。

所以iilter需要的函数里面必须返回的是true或者false值以供判断。

2.10.1 去空字符串

把一个Array中的空字符串删掉,可以这么写:

let arr = ['A', '', 'B', null, undefined, 'C', '  '];
let r = arr.filter(function (s) {
    return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
r; // ['A', 'B', 'C']
  • trim() 方法用于删除字符串的头尾空白符,空白符包括:空格、制表符 tab、换行符等其他空白符等。

  • trim() 方法不会改变原始字符串。

  • trim() 方法不适用于 null, undefined, Number 类型。

2.10.2 去重复元素

利用filter,可以巧妙地去除Array的重复元素:

let
    r,
    arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

r = arr.filter(function (element, index, self) {
    return self.indexOf(element) === index;
});

console.log(r);
  • 函数中的参数element来自每次对arr的遍历,index是这个遍历拿到的参数在arr的索引(当前索引),self指代arr本身。
  • 尽管arr遍历只能拿到一个参数,但是indexself这些是自己出来的,所以不会保参数未定义的的错误。
  • 但是这只是对于数组array有的,对于对象这种没有索引的,他是没用的。
  • indexOf可以拿到该元素在参数(此处是self,即arr)里面的第一次出现的位置。

2.10.3 非要用filter处理对象

const obj = {
    name: "xujun",
    age: 15,
    school: "SICAU",
    null: false,
};

// obj --> array
const objToArray = Object.entries(obj); 

objToArray.filter(function (element, index, self) {
    console.log(`元素: ${element},索引: ${index},原数组:`, self); 
    // 注意self在模版字符串外面
    return element[1]; // 注意这里必须加索引,不然每次的element是一个数组,如['null', false],他是true的
});
console.log(obj);

2.10.4 找素数

function get_primes(arr) {
    const result = arr.filter(function (x) {
        if (x === 2) {
            return true; //2 是素数
        } else if (x === 1) {
            return false; // 1 不是素数
        }

        const max = Math.sqrt(x);           // 用当前数x的平方根,不是数组最大值!
        for (let i = 2; i <= max; i++) {    // i从2开始
        //for (let i = 2; i * i <= x; i++) {   评论区看到的,这个方便
            if (x % i === 0) {              // 取模判断整除(最标准)
                return false;               // 能整除 → 不是质数,直接过滤
            }
        }
        return true; // 循环结束都没整除 → 是质数,保留
    });
    return result;  // 返回筛选后的质数数组
}

let
    x,
    r,
    arr = [];
for (x = 1; x < 100; x++) {
    arr.push(x);
}

r = get_primes(arr);
if (r.toString() === [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97].toString()) {
    console.log('测试通过!');
} else {
    console.log('测试失败: ' + r.toString());
}

2.11 sort

Arraysort()方法默认把所有元素先转换为String再排序。

注意风险

奇怪的结果:

  • '10'排在'2'的前面,因为字符'1'比字符'2'的ASCII码小。
  • appleGoogle后面(确实如此,但是顺序是实力从强到弱还是从弱到强?)
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
注意

最后友情提示,sort()方法会直接对Array进行修改,它返回的结果仍是当前Array

let a1 = ['B', 'A', 'C'];
let a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一对象

2.12 forEach

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

let arr = ['Apple', 'pear', 'orange'];
arr.forEach(x=>console.log(x)); // 依次打印每个元素

2.13 闭包函数 Closure

高阶函数:将函数作为参数

闭包函数:将函数作为返回值,可以就可以让他不会立刻执行,只有再次调用才会执行。

function lazy_sum(arr) {
    let sum = function () {
        return arr.reduce(function (x, y) {
            return x + y;
        });
    }
    return sum;
}
let f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
重要

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

因为这种闭包的返回函数是最后执行的,他每次指向的循环变量是最后一次循环的那个变量,而非循环中的变量。

错误示范(这里面的i都是3:

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;
f1(); // 16
f2(); // 16
f3(); // 16

正确示范方法一:

i参数传给函数,然后把这个参数保存在闭包里面,而且这里使用了一个立即执行的函数语法

(function (x) {
    return x * x;
})(3); // 9
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();

f1(); // 1
f2(); // 4
f3(); // 9

正确示范方法二:

跟错误示范里面差别在varlet

循环声明作用域闭包捕获f1()f2()f3()
var i函数作用域(共用)同一个 i161616
let i块级作用域(独立)独立的 i149
function count() {
    let arr = [];
    for (let i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}
let [f1, f2, f3] = count();
f1(); // 16

2.14 箭头函数

(x, y, ...rest) => {
    let i, sum = x + y;
    for (i=0; i<rest.length; i++) {
        sum += rest[i];
    }
    return sum;
}

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。

回顾前面的例子,由于JavaScript函数对this绑定的错误处理,下面的例子无法得到预期结果:

let obj = {
    birth: 1990,
    getAge: function () {
        let b = this.birth; // 1990
        let fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

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

let obj = {
    birth: 1990,
    getAge: function () {
        let b = this.birth; // 1990
        let fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 25
重要
  1. 对象的普通方法(外层函数)this 指向 调用它的对象(obj)
  2. 嵌套的普通函数(内部显式函数) → 单独执行时,this 丢失绑定 → 指向 window / undefined
  3. 嵌套的箭头函数没有自己的 this,直接继承外层的 this → 指向 obj

2.15 标签函数

标签函数和普通函数的定义区别仅仅在于参数,它接收的参数是 JS 引擎自动传入[静态文本数组, 插值1, 插值2, ...]

如果我们想对数据库进行修改,完全可以定义一个标签函数如下:

function update(strings, ...exps) {
    let sql = strings.join('?');
    // 执行数据库更新
    // TODO:
}

函数调用可以简化为带标签的模板字符串:

let id = 123;
let age = 21;
let score = 'A';

update`UPDATE users SET age=${age}, score=${score} WHERE id=${id}`;

2.16 生成器

生成器(generator)是ES6标准引入的新的数据类型。一个生成器看上去像一个函数,但可以返回多次。

generator跟函数很像,定义如下:

注意多出的*号),并且,除了return语句,还可以用yield返回多次。

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}