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可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于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可以拿到xiaoming的birth属性。
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():逐个处理元素,输出新数组 → 对应分布式 MapJS
array.reduce():把一堆值聚合成一个结果 → 对应分布式 Reduce
Google 那篇论文,就是把这个简单思路,放大到几千台机器的大数据场景。

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个参数:
- 当前元素的值
- 当前元素的索引
- 原数组本身
2.9.2.2 parseInt 函数的参数规则
parseInt 是原生转换函数,接收两个必填参数:
- 待转换的字符串
- 基数(进制数,如10代表十进制、2代表二进制)
2.9.2.3 逐次执行过程拆解
arr.map(parseInt) 等价于把索引当成进制传给 parseInt,执行流程:
parseInt('1', 0):基数0默认按十进制解析 → 结果1parseInt('2', 1):基数1为无效进制 → 结果NaNparseInt('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]
- 禁止直接用
arr.map(parseInt),会因索引被当作进制导致转换失败 - 用
parseInt必须通过箭头函数手动传参+指定十进制 - 简单字符串转数字,优先用
arr.map(Number),无参数陷阱
2.10 filter
filter只能在数组上用。
和map()类似,Array的filter()也接收一个函数。和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遍历只能拿到一个参数,但是index、self这些是自己出来的,所以不会保参数未定义的的错误。 - 但是这只是对于数组
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
Array的sort()方法默认把所有元素先转换为String再排序。
奇怪的结果:
'10'排在'2'的前面,因为字符'1'比字符'2'的ASCII码小。apple在Google后面(确实如此,但是顺序是实力从强到弱还是从弱到强?)
['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
正确示范方法二:
跟错误示范里面差别在var和let
| 循环声明 | 作用域 | 闭包捕获 | f1() | f2() | f3() |
|---|---|---|---|---|---|
var i | 函数作用域(共用) | 同一个 i | 16 | 16 | 16 |
let i | 块级作用域(独立) | 独立的 i | 1 | 4 | 9 |
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
- 对象的普通方法(外层函数) →
this指向 调用它的对象(obj) - 嵌套的普通函数(内部显式函数) → 单独执行时,
this丢失绑定 → 指向 window / undefined - 嵌套的箭头函数 → 没有自己的 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;
}