JS 廖雪峰教程笔记

数据类型

  1. number:整数、浮点数、科学计数法、NaN和Infinity;

    注意一: NaN===NaN//false,只能通过isNaN()来判断。

    注意二: 浮点数相等比较不能直接0.1+0.2 === 0.3;而是Math.abs(0.1+0.2-0.3<0.000001),一般六位小数精度足够了。

  2. string:字符串;
  3. boolean:布尔值;

    注意: 控制台输入console.log(typeof 1>3);//falseconsole.log(typeof (1>3))//boolean是不一样的。

  4. null:空值,事实上应该是一个对象:typeof null//object;
  5. undefined: 未定义的空值。

    注意: 若要对变量显式定义undefined,应该var a = void 0;

  6. 数组:字面量[1,2,3]new Array(1,2,3),一般我使用字面量方法,可读性强;

    注意: typeof [1,2] === 'object',[1,2] instanceof Array === true[1,2] instanceof Object === true;

  7. 对象:字面量{a:1,b:'2'}

变量

使用关键字var声明变量,如果缺少var,变量将自动提升为全局变量。这可能会出现问题:污染全局作用域,变量命名冲突。最好是使用严格模式或者JS模块化。

全局模式启用:在js文件第一行加上use strict

字符串

  1. 单行字符串:使用’ ‘或” “包裹内容,换行使用\n
  2. 多行字符串:ES6中,使用` `反引号可以省略换行,且与单行字符串带\n是严格相等的,如:

    1
    2
    3
    4
    5
    var a = `你好
    我是
    周聪`;
    var b = '你好\n我是\n周聪';
    //(a===b)===true;
  3. 模板字符串
    我们都知道使用+号进行字符串拼接:

    1
    2
    3
    var name = "周聪";
    var sayHi = "Hi,"+name;
    console.log(sayHi);

    但是如果变量较多,+号和”双引号可能出现遗漏。在ES6中,使用模板字符串可以减少工作量,避免错误。

    1
    2
    var newSayHi = `Hi,${name}`;
    console.log(newSayHi);

    注意: 模板字符串必须用反引号包裹。

  4. 常用属性、方法

    • 字符串一经定义,不可局部修改:name[1] = "a";//修改无效,name === "周聪",下面的方法也不会修改原字符串的值,而是返回一个新的字符串;
    • string.length:获取字符串长度,可以用下标进行索引;
    • String.toUpperCase():将一个字符串中的英文字母全部替换为大写形式;
    • String.toLowerCase():将一个字符串中的英文字母全部替换为小写形式;
    • String.indexOf(substring):搜索指定子串出现的位置。找到返回子串在字符串中出现的位置,未找到返回-1;
    • String.substring(start,end):截取字符串,第一个参数是起始位置,默认为0,第二个参数是终点位置。区间表示为[start,end)。

数组

  1. Array.length:获取数组长度。如果修改length属性,数组的内容也会改变。减少length会截断多余的数组项,增加length会自动填充undefined值。若要修改的数组项的索引值超过length大小,则数组自动扩充,中间用undefined填充,最后一个值为新修改的项;
  2. Array.indexOf():与字符串的方法一样,返回搜索项的索引值,未找到则返回-1;
  3. Array.slice(start,end):相当于字符串中的substring()方法,,截取部分元素,返回一个新的Array数组。不传参数会复制原数组。

    1
    2
    3
    var arr = [1,2,3,4,5];
    var copyArr = arr.slice();
    copyArr === arr;
  4. push和pop:push是在Array末尾添加元素;pop是从末尾移除元素,并且返回被移除的元素;

  5. unshift和shift:unshift是在Array首部添加元素,shift是删除并返回第一个元素;
    模拟队列: 队列是先进先出,首进尾出,因此结合unshift和pop方法可以模拟一个队列;
    模拟堆栈: 堆栈是先进后出,首进首出,使用unshift和shift即可。
  6. Array.sort():修改当前数组各项顺序,可以传入一个比较函数自定义比较数据。该方法效率相比较与手写排序要高很多。
  7. Array.reverse(): 翻转当前数组;
  8. Array.splice(): splice()方法是修改数组的万能方法,可以从指定的索引开始删除若干元素,再从该位置添加若干元素。
    • 只删除,不添加:arr.splice(2,2);//从索引2开始删除2个元素
    • 只添加,不删除:arr.splice(2,0,'first','second','third');//从索引2开始添加3个元素
    • 修改某个元素:arr.splice(2,1,'new');//修改索引为2的元素
    • 添加多个元素,删除多个元素: arr.splice(2,10,'new','new','new');//最常用
  9. Array.concat(): 将当前的数组与另一个数组连接,另一个数组接在当前数组的后面,返回一个新数组。事实上,concat函数的参数并不要求必须为数组,可以传入任意个值,然后将这些值依次push进原数组。
  10. Array.join():将数组中各项转为字符串后用指定字符串相连接,默认为逗号,
  11. 多维数组:var arr = [[1, 2, 3], [400, 500, 600], '-'];;

对象

对象由若干键值对组成,键值对间用逗号隔开,最后一个键值对后面不要加逗号。访问属性可以用.点也可以用[]中括号,中括号中是属性名字符串。

检测某属性是否存在于对象中,可以使用in操作符:'name' in Person;,属性名要带引号。如果要遍历对象中所有属性,可以利用for…in循环:

1
2
3
for(var key in obj){
console.log(key);
}

如果要避免继承来的属性干扰,可以用hasOwnProperty()方法。

Map和Set

JS的对象是键值对的集合,相当于其他编程语言的Map。但是对象的键必须是字符串,不能是Number或是其他数据类型。为了解决这个问题,ES6引入了新的数据类型Map。

Map

Map是一对键值对的集合,具有极快的查找速度。

1
2
3
4
5
6
7
8
9
10
11
12
//初始化Map需要一个二维数组,或者空Map调用set方法赋值
var m = new Map([['adult',18],['child',12],['elder',60]]);
//添加或修改键值对
m.set('baby',3);
//检测是否存在某键
m.has('adult');
//获取键对应的值
m.has('adult');
//删除某个键值对
m.delete('adult');
//获取Map项的数目
m.size;

Set

Set只存储键,不存储值,且键唯一不重复。

初始化Set对象可以传入一个数组或创建空Set。数组中重复的值会被过滤,因此可以用来数组去重。

Set对象有以下几种:

  1. add():添加键,重复添加键不影响Set的唯一性;
  2. delete():删除指定键,若删除成功,返回true;若键不存在,返回false;
  3. clear():清空Set;
  4. has():检测Set中是否存在某个键;
  5. forEach():根据集合中元素的顺序,对每个元素都执行提供的 callback 函数一次。callback 有三个参数:元素的值,元素的索引和将要遍历的集合对象。

iterable类型

虽然Map和Set有一些新特性,弥补Array的一些缺陷,但是仍然存在一个问题:无法使用下标遍历。因此,ES6新增iterable类型统一Map、Set和Array类型,使用for…of来遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [1,2,3];
var s = new Set(a);
var m = new Map([[1,1],[2,2],[3,3]]);

for(var x of a){
console.log(x);
}
for(var x of s){
console.log(x);
}
for(var x of m){
console.log(`${x[0]}:${x[1]}`);
}

看上去,for…of和for…in很相似,但是for…in在遍历这些对象时,会遍历自定义属性。比如arr.name="周聪",此时for…in会输出name属性值。但是for…of不会,它只循环集合本身内容。

此外,iterable类型自带forEach方法,每次迭代都会执行一次回调函数。

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
a.forEach(function(ele,index,array){
/**
* ele: 当前迭代的元素值;
* index:当前元素索引,Set对象中该参数与第一个参数值相同;
* array:迭代对象本身,此处指a数组
*/
console.log(ele+':'+index);
});

函数

函数是JS的一等公民,可以当作参数传递。具有抽象性。

函数定义

  1. 函数声明

    1
    2
    3
    function f(x){
    return x;
    }
  2. 函数表达式声明

    1
    2
    3
    var f = function(x){
    return x;
    }

如果函数不执行return语句,返回默认值undefined。

函数传参

JS中,调用函数时并不强制传参数量与声明时参数数量一致,如果调用时传参比声明时少,会自动传入undefined代替,因此在声明时要对参数进行判断。

在函数中,JS指定了一个名叫arguments变量,其中包含了所有在调用函数时传递的参数。

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

从上面的例子中可以看到arguments不需要声明,因为是JS内置的。而且它和数组相似,具有length属性,且通过下标可以获取元素值。但它并不是数组,是另一种对象。

1
2
3
```
arguments instanceof Array;//false
arguments instanceof Object;//true

有时候,我们在函数声明时显式定义了a、b两个参数,再判断实际调用时是否传入c、d、e等参数。如果我们要将a、b分成一组,c、d、e分成一组,第二组的写法可能是var rest = [arguments[2],arguments[3],arguments[4]];,看上去很麻烦。

在ES6中,可以使用rest参数来改写函数:

1
2
3
4
5
6
function x(a,b,...other){
console.log(other);
console.log(other.length);
console.log(other[0]);
}
x(1,2,3,4,5);

输出:

1
2
3
(3) [3, 4, 5]
3
3

因此,arguments = 显式声明形参 + rest形参。
注意: rest参数只能写在最后,前面用…标识。

函数变量作用域

  1. 内部作用域

    如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量。但是,函数内部可以使用函数外部的变量。

    在函数中还存在变量提升的现象。指的是函数在执行前,会先扫描函数体,将所有内部声明的变量都提升到函数顶部,但不会提升变量的赋值。举个例子:

    1
    2
    3
    4
    5
    function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
    }

    实际上,相当于:

    1
    2
    3
    4
    5
    6
    function foo() {
    var x,y;
    x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
    }

    这样,x在赋值时不会报错,但是此时y的值还没有变成’Bob’,而是undefined。
    为了安全起见,函数内的变量声明赋值应该在函数开头完成。

  2. 全局作用域

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

    1
    2
    var globalVar = 'a';
    window.globalVar === globalVar;//true

    之前说过函数内部可以调用外部变量,是因为函数在搜索变量时会先在内部查找,如果没有找到则向上层函数作用域中查找。而全局作用域作为顶级作用域,如果其中也没有要查找到的变量,则查找失败,报ReferenceError错误。

  3. 命名空间
    由于全局作用域唯一,而所有JS文件都可以在其中声明自己的变量,往往会导致不同JS文件出现命名冲突的情况。因此建议声明自己的变量和函数时,将它们绑定在一个个性化全局对象中。虽然仍有可能出现冲突,但出现率会大大减少。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 唯一的全局变量MYAPP:
    var MYAPP = {};

    // 其他变量:
    MYAPP.name = 'myapp';
    MYAPP.version = 1.0;

    // 其他函数:
    MYAPP.foo = function () {
    return 'foo';
    };
  4. 块级作用域

    有点JS基础的应该都知道,在JS中,if语句、for循环等块级代码中声明的变量实际上是声明在全局作用域中的。如果在块级代码以外调用,不会出现未声明的错误。

    这可能会导致一些问题:

    1
    2
    3
    4
    5
    var sum = 0;
    for(var i = 0;i<10;i++){
    console.log(i);
    }
    i++;//10

    在ES6中,有一个新的关键字:let。使用let声明的变量在块级代码执行后不会保留:

    1
    2
    3
    4
    for(let i =0 ;i<10;i++){

    }
    i++;//SyntaxError
  5. 常量
    ES6引入关键字const用来声明常量。需要注意以下几点:

    1. const声明常量和let、var在声明变量后不能再使用其他两种关键字声明同名变量;
    2. const常量声明后不得修改,不然怎么叫常量呢。
  6. 解构赋值
    这也是ES6中的语法,用来对一组变量进行赋值。
    在ES6出现之前:

    1
    2
    3
    4
    var arr = [1,2,3];
    var a = arr[0];
    var b = arr[1];
    var c = arr[2];

    在ES6中:

    1
    2
    3
    4
    var [a,b,c] = arr;
    console.log(a);
    console.log(b);
    console.log(c);

    也就是说要从数组取值时,应该将一组变量用[]括起。如果有多层嵌套:

    1
    var arr = [1,[2,3]];

    那么解构赋值的括号嵌套也要保持一致:

    1
    var [a,[b,c]] = arr;

    如果部分值不需要,可以忽略:

    1
    var [,,c] = arr;

    如果从对象中取值,用{}括起,且变量名与属性名一致,嵌套也要一致。下面是一个简单的例子:

    1
    2
    3
    4
    5
    6
    7
    var obj = {
    x:1,
    y:{
    z:3
    }
    };
    var {x,y:{z}} = obj;

    如果存在已经声明的变量,不能直接省略var,而是用另一种表达式:

    1
    2
    var x,y
    ({x,y} = obj);

    解构赋值还有一个常见的用法,交换两个数的值:

    1
    2
    var j = 0,k =1;
    [j,k] = [k,j];

方法

  1. 方法是对象的函数。

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

    xiaoming.age; // function xiaoming.age()
    xiaoming.age(); // 今年调用是25,明年调用就变成26了

    在age方法中我们使用了this关键字,该变量指向当前调用方法的对象。如果直接调用age();,此时this指向全局对象window;而如果在严格模式下,this的值是undefined。

  2. apply方法
    apply方法可以指定方法中this的值。该方法接收两个参数,第一个是this指定的对象,另一个是传入方法的参数组成的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
    }

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

    xiaoming.age(); // 25
    getAge.apply(xiaoming,[]); // 25, this指向xiaoming, 参数为空
  3. call方法
    此外,函数还有一个call方法,它与apply方法类似,但是其不要求参数组成一个数组。

那么,什么时候使用apply和call?
事实上,他们两个没有特别大的区别,只有当参数是数组时,apply有天生优势。

高阶函数

当一个函数接受的参数中有函数变量时,该函数称为高阶函数。

JS中常见的高阶函数有:map、reduce、filter和sort等等。

  1. Array.map(callback)

    map方法可以对数组中的每一个元素执行一遍函数形参,返回一个新数组,也就是在原数组和新数组之间增加一种映射关系。

    其函数形参接收3个参数:n当前迭代项的值,index当前迭代项的索引,arr当前迭代的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    function addTen(n,index,arr){
    console.log(arr);
    console.log(index);
    return n+10;
    }
    var origin = [1,2,3,4,5,6];
    var result = origin.map(addTen);
    console.log(result);

    这里有一个传参的错误实例:

    1
    2
    3
    4
    5
    6
    'use strict';

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

    输出:

    1
    1,NaN,NaN

    原因是parseInt(string, radix)接收2个参数,第一个是要转换的字符,第二个是进制。但是map方法默认第二个传参为索引值。导致实际运算的真实情况是

    1
    2
    3
    parseInt('0', 0); // 0, 按十进制转换
    parseInt('1', 1); // NaN, 没有一进制
    parseInt('2', 2); // NaN, 按二进制转换不允许出现2
  2. Array.reduce(callback)

    reduce函数的函数形参f接收2个参数,第一个参数为上一次迭代时运算的结果,第二个参数是参与本次运算的数组项。

    回调函数中可以传递3个参数:第一个是上一次运算的结果值,第二个是当前迭代的计算值,第三个是当前计算值的索引下标,第四个是调用函数的数组对象。
    效果如下:
    [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4);
    相当于:

    1
    2
    3
    4
    5
    6
    function f(x,y){
    return x+y;
    }
    y1 = x1+x2;
    y2 = y1+x3;
    y3 = y2+x4;
  3. Array.filter函数

    filter(callback)用于把Array的某些元素过滤掉,然后返回剩下的元素。根据数组每一项在执行回调函数后返回的布尔值确定是否保留该数组项。

    例如删除偶数:

    1
    2
    3
    4
    var arr = [1, 2, 4, 5, 6, 9, 10, 15];
    var r = arr.filter(function (x) {
    return x % 2 !== 0;
    });

    与map和reduce的回调函数相同,filter的回调函数也有3个参数,意义与之前的两个方法一样。

  4. Array.sort(compare)
    sort方法可以传回调函数参数也可以不传。如果传入自定义的比较函数,则函数的返回值应符合如下规则:

    1. x<y:返回负值;
    2. x=y:返回0;
    3. x>y:返回正值;

    如果不传比较函数,则默认将数组项转成字符串,按ASCII码大小排序。

闭包

闭包是在函数中声明函数,声明的子函数能够使用外层函数中定义的变量。当外层函数返回内部函数时,内部函数在调用时仍保存这些外层变量。

但是被返回的函数并不是立即执行的,必须手动调用。而此时,内部保存的变量状态可能会变化。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function count(){
var arr = [];
for(var i = 1;i<=3;i++){
console.log(i);
arr.push(function(){
console.log(i);
return i*i;
});
}
console.log(arr);
return arr;
}

var result = count();
var r1 = result[0];
var r2 = result[1];
var r3 = result[2];

r1();
r2();
r3();

由于result中每一项的函数中保存了变量i,而i又在循环后变成了4,所以r1/r2/r3三个函数最后输出的都是4*4=16。
要修改成输出1,4,9,应该在循环中使用let声明i。每一次循环,都相当于执行了一次let i,由于let只存在于块作用域中,所以出了本次循环后别的循环中的声明不再对它有影响。

在不支持ES6的浏览器中,可以使用立即执行匿名函数表达式的方法:

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

在要返回的函数外再加一层匿名函数,并将i作为参数传递,同时立刻执行该匿名函数。这时,内部函数引用的是匿名函数的形参,该形参的值与i相等,但并不会再因为i改变。

匿名函数立即执行写法:(function(){})();,用()括起写成函数表达式,避免JS语法解析报错。

使用闭包可以实现一般面向对象中的private私有变量。

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

function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}

var obj = new create_counter();
obj.x;//undefined
obj.inc();//1

可以看到obj是无法获取到x值,但是能够修改x的值。这样就保证了x变量私有化。

箭头函数

箭头函数是匿名函数的另一种写法:

1
2
3
4
5
6
//箭头函数
x => x*x;
//一般写法
function(x){
return x*x;
}

箭头函数相当于匿名函数,并且简化了函数定义。如果函数体中只包含一条表达式,可以省略{}和return,但如果有多个语句或返回值为对象字面量时,不能省略{}和return。

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

如果参数有0个或2个及以上,需用()括起。

箭头函数与匿名函数有一个区别,其内部的this变量作用域根据上下文确定。
举一个匿名函数中this指向错误的例子:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
birth:1990,
getAge:function(){
var b = this.birth;
var fn = function(){
console.log(this.birth);
};
return fn();
}
}
obj.getAge();//undefined

虽然我不知道为什么要写成匿名函数的形式,直接在getAge里执行不可以吗?但是廖雪峰这么举例子,那就这样吧。

而修改成箭头函数:

1
2
3
4
5
6
7
8
9
var obj = {
birth:1990,
getAge:function(){
var b = this.birth;
var fn = () => console.log(this.birth);
return fn();
}
}
obj.getAge();//1990

又因为箭头函数中的this已经根据上下文绑定,指向外层定义者obj,所以使用apply或call方法都无法改变其this指向。

generate生成器

ES6提出generate生成器概念,它是一种数据类型,跟函数很像:

1
2
3
4
5
6
function* foo(x){
yield x+1;
yield x+2;
return x+3;
}
var f = foo(3);

与普通函数不同之处:使用function*定义,除了return语句还可以使用yield返回多次。上面的例子中f是一个generate对象,想要获得foo执行返回的值,需要使用Generate.next()方法,该方法返回一个对象{value:值,done:true/false}

流程应该是:

  1. 调用f.next(),执行一遍foo,
  2. 遇到yield关键字,获取返回值,
  3. 判断该值是否为最后一个返回值,如果是则done属性的值为true,否则为false。

除了使用next方法,还可以用for…of方法迭代generate对象,这时不需要我们手动判断done属性。

1
2
3
for(var f of fib(10)){
console.log(f);//f的值就是value属性的值
}

由此可见,generate对象可以在要返回一组数据时,代替数组作为返回值这种方式。
此外,generate还有优化ajax代码的作用,实现一个异步方法。
Generator 函数的含义与用法

标准对象

使用typeof操作符区分数据类型:

1
2
3
4
5
6
7
8
9
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'

可以区分的有number、string、boolean、undefined、function和object六个基本数据类型,typeof null返回”object”。

基本数据类型还有对应的封装对象类型:

1
2
3
var n = new Number(123);
var b = new Boolean(true);
var s = new String("string");

这三个封装函数如果在赋值时不使用new关键字生成对象实例,功能会变成基本类型转换:

1
2
3
var n = Number("123");//123
var b = Boolean(0);//false
var s = String(123);"123"

要明确某个对象是否为数组,可以使用Array.isArray()方法。是数组会返回true,不是则返回false。

最后,虽然大多数数据类型都有toString方法,但是number数据调用toString需要特殊处理,否则会报错。

1
2
123..toString(); // '123', 注意是两个点!
(123).toString(); // '123'

Date对象

顾名思义,Date对象表示日期时间。创建的默认实例是代码执行的时间。
想要创建指定时间的Date实例,有两种方法:

1
2
3
4
5
6
7
8
9
//第一种方法
var date = new Date(2020,0,1,0,0,0,0);
//参数依次为年、月(从0开始)、日、时、分、秒。

//第二种方法
var dateStamp = Date.parse('2020-01-01-24T00:00:00.0+08:00');
//这是用parse方法解析一个符合ISO 8601格式的字符串,返回值是一个时间戳
var date = new Date(dateStamp);
//通过时间戳再创建Date实例。

Date对象具有的方法如下:

1
2
3
4
5
6
7
8
9
10
11
var date = new Date();
date.getFullYear();//获取年份
date.getMonth();//获取月份,月份并不是1~12,而是0~11
date.getDate();//获取日期
date.getDay();//获取星期几
date.getHours();//小时,24小时制
date.getMinutes();//分钟
date.getSeconds();//秒
date.getMillseconds();//毫秒
date.getTimes();//number类型时间戳
Date.now();//获取当前时间

正则对象RegExp

正则表达式是匹配字符串的强大武器。
廖雪峰老师的教程讲的不是很详细,建议看看JavaScript正则迷你书

JSON对象

json格式与JS对象字面量的格式一致。目前都用JSON取代XML传输报文格式。

JSON对象可以序列化为字符串:

1
2
3
4
5
6
var json = {
name:'周聪',
age:22,
gender:"male"
}
var jsonString = JSON.stringify(json);

JSON.stringify接收3个参数:

  1. value:要序列化的值;
  2. replacer:如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为null或者未提供,则对象所有的属性都会被序列化;
  3. space:指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(字符串的前十个字母),该字符串将被作为空格;如果该参数没有提供(或者为null)将没有空格。

具体例子应该想一想就知道,我就不举例子啦~

如果我们获取的是一个JSON格式的字符串,可以使用JSON.parse方法将其解析成JS对象。

1
2
var jsonString = '{"name":"周聪","age":22}';
var json = JSON.parse(jsonString);

面向对象编程

创建对象

创建对象首先要有一个构造函数:

1
2
3
4
5
6
7
function Person(name){
this.name = name;
}

Person.prototype.sayHello = function(){
console.log("hello "+this.name);
}

一般将属性声明在构造函数内部,公用方法声明在构造函数的原型上。如果用new关键字调用,Person构造函数返回的就是this值,而如果不用new关键字,返回的是undefined。所以一定不要忘记使用new关键字。但是,为了防止没有new的出错情况,需要修改代码,提高健壮性:

1
2
3
4
5
6
7
8
9
function person(name){
var obj = new Object();

obj.name = name;
obj.sayHello = function(){
console.log("hello "+this.name);
}
return obj;
}

因为这不算是一个构造器函数,所以函数名首字母就不大写了。

class继承

ES6中使用关键字class来简化类的定义。

1
2
3
4
5
6
7
8
9
class Person{
constructor(name){
this.name = name;
}

hello(){
console.log(`hello ${this.name}`);
}
}

此外,还是用extends关键字简化类的继承。

1
2
3
4
5
6
7
8
9
10
class Man extends Person{
constructor(name,age){
super(name);//使用super调用父类构造函数
this.age = age;
}

showAge(){
console.log(`I am ${this.age} years old`);
}
}

class关键字与原型继承的实现原理相同,但是代码简化了很多。

浏览器

浏览器 渲染引擎 JS引擎
IE Trident Chakra (查克拉)
Chrome Blink V8
FireFox Gecko OdinMonkey
Safari Webkit SquirrelFish(松鼠鱼)

浏览器对象

在用户浏览网页的时候,浏览器会自动创建一些对象,这些对象存放着浏览器窗口的属性和相关信息,也就是大家熟称的BOM。浏览器对象模型(BOM)定义了很多浏览器对象,包括window、navigator、screen、location、document和history等。

  1. window对象
    window对象除了作为顶级全局作用域外,还保存当前浏览器窗口的部分属性。比如innerWidthinnerHeight属性,指浏览器窗口内部宽高(不包括工具栏、菜单栏等高度)。而outerHieghtouterWidth属性可以获取全部宽高。
  2. navigator对象
    navigator对象指代浏览器的代理信息。常用属性有

    1. navigator.appName:浏览器名称;
    2. navigator.appVersion:浏览器版本;
    3. navigator.language:浏览器设置的语言;
    4. navigator.platform:操作系统类型;
    5. navigator.userAgent:浏览器设定的User-Agent字符串。

    一般判断浏览器类型,不推荐直接根据navigator的属性判断,而是根据某些特定属性是否存在来判断:

    1
    2
    3
    //IE9以下获取浏览器宽度用document.body.clientWidth
    //IE9以上使用window.innerWidth
    var width = window.innerWidth || document.body.clientWidth;
  3. screen对象
    screen对象表示屏幕的信息,常用的属性有:

    1. screen.width:屏幕宽度,以像素为单位;
    2. screen.height:屏幕高度,以像素为单位;
    3. screen.colorDepth:返回颜色位数,如8、16、24。
  4. location对象

    location对象表示当前页面的URL信息。
    常用属性有:

    1. location.protocol:url的传输协议,包括:
    2. location.host:url的域名+端口号;
    3. location.hostname:url的域名;
    4. location.port:url中的端口;
    5. location.pathname:域名后的完整文件目录;
    6. location.search:文件目录后的参数,包括?
    7. location.hash:#后的hash参数。

    例如,一个完整的URL:

    http://www.example.com:8080/path/index.html?a=1&b=2#TOP
    可以用location.href获取。要获得URL各个部分的值,可以这么写:

    1
    2
    3
    4
    5
    6
    location.protocol; // 'http:'
    location.hostname; // 'www.example.com'
    location.port; // '8080'
    location.pathname; // '/path/index.html'
    location.search; // '?a=1&b=2'
    location.hash; // '#TOP'

    要加载一个新页面,可以调用location.assign()。如果传入的url不带http等协议,url会默认连接在当前页面的url后。

    如果要重新加载当前页面,调用location.reload()方法非常方便。

  5. document对象

    document对象指代当前页面,是整个DOM的根节点。比如可以使用document.title修改当前页面的标题。还可以使用document.querySelector()获取元素节点。此外,document还有cookie属性,获取当前页面存储在用户客户端中 的cookie。

  6. history对象

    history对象保存了浏览器的历史记录,JavaScript可以调用history对象的back()或forward (),相当于用户点击了浏览器的“后退”或“前进”按钮。但是现在前后端交互往往使用Ajax来处理,简单粗暴的history.back会影响用户体验。因此不建议使用history对象。

更新DOM

HTML文档中的节点总体上被看作一颗DOM树上的节点,对DOM的操作与对树的操作一样,有修改、删除、插入遍历等操作。当然,首先得获取要操作的节点:getElementById()、getElementsByTagName()、getElementsByClassName()、querySelector()、querySelectorAll()等方法都可以按需使用。

  1. 修改节点内容

    修改内容有两种方式,一种是修改文本:对innerText或textContent属性赋值即可;还有一种是修改内部html文档:修改innerHtml属性,将节点内的所有子结构都替换为要修改的内容。

  2. 插入新节点

    在目标节点的基础上插入原始节点的第一种方法:destEle.appendChild(oriEle),将oriEle节点插入到destEle节点中。如果oriEle节点已经存在与文档流中,将会被剪切到目标节点。当然也可以使用document.createElement()来创建一个新的节点,使用appendChild方法插入。这种插入方法会将子节点插入在父节点最后。

    另一种方法是使用insertBefore()方法,传入两个参数:插入子节点和参照物子节点。作用是将插入子节点插入在参照物子节点之前。

  3. 删除节点

    删除节点只有一个方法:removeChild()。要想删除某个节点,先获取其父元素节点Node.parentElement,然后父元素节点调用removeChild方法即可。这种删除方法,删除的节点仍保存在内存中,但是文档流中已经不存在其位置了。

Ajax

Ajax的意思是使用JS执行异步网络请求。核心思想是使用XMLHttpRequest对象来传输报文。

手写一个兼容低版本IE的Ajax代码:

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
var request;
if(window.XMLHttpRequest){
request = new XMLHttpRequest();
}else{
request = new ActiveXObject('Microsoft.XMLHTTP');
}

function success(text){
console.log(text);
}

function fail(){
console.log('fail');
}

request.onreadystatechange = function(){
if(request.readyState === 4){
//请求成功
if(request.status === 200){
//200成功
return success(request.responseText);
}else{
//失败
return fail();
}
}else{
//继续请求。
}
}

request.open('GET','/api/categories?id=1');
request.responseType = "json";
request.setRequestHeader("Accept", "application/json");
request.send();

上面的代码中,根据是否能够使用XMLHttpRequest对象,判断用户使用的是否为现代浏览器。如果XMLHttpRequest对象无法使用,则说明用户使用的是低版本IE,此时用使用ActiveXObject对象来传输报文。

此外,还要考虑GET请求和POST请求的区别。GET请求不需要向send()方法传递额外参数,POST请求需要将body中的数据以字符串或formData对象形式传递。

1
2
3
request.open('post','/api/categories');
request.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
request.send('id=1');

然后,XMLHttpRequest对象有5种状态readyState,当状态改变时触发onreadystatechange回调函数。

状态 描述
0 UNSENT 代理被创建,但尚未调用 open() 方法。
1 OPENED open() 方法已经被调用。
2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
3 LOADING 下载中; responseText 属性已经包含部分数据。
4 DONE 下载操作已完成。

若要判断请求的数据返回成功,同时根据readyState和status两个来判断即可。status即http协议的状态码。

最后是请求完成后,XMLHttpRequest对象的responseText中会保存后台返回的数据,一般是一个JSON格式文本。

关于跨域以及解决方案,可以看看这篇。一般解决方案是jsonp和cors。

Promise

JS的事件都是单线程执行,这也导致一些操作必须使用异步回调的方式。而Promise出现的目的就是实现异步程序的另一种解决方案,避免了回调地狱。

Promise是一个容器,存放着某个未来才结束的事件。它提供统一的API,对所有异步操作都使用同一种方法处理。

Promise对象一共有3种状态:pending(等待异步事件执行)、fulfilled(事件执行成功)、rejected(事件执行失败)。Promise对象的状态转换只有p->f和p->r两种,通过用户定义的resolve和reject函数 来改变状态。一旦状态转换完成,Promise对象的状态定型,此时叫resolved状态。如果在控制台中打印fulfilled状态的Promise对象,其[[PromiseStatus]]值为resolved。因此可以将resolved与fulfilled等价。

  1. 创建Promise对象。

    使用new关键字创建Promise对象,传入一个函数参数,该函数带有2个函数参数:resolve和reject。resolve函数作用是Promise的状态改变为fulfilled,而reject函数作用是将Promise状态改为rejected。

    举个例子:

    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
    //创建Promise对象实例,此时,内部函数的语句将立刻执行。
    var promise = new Promise(function(resolve,reject){
    //定义异步任务1+2,返回值保存在result变量中
    var result = 1+2;
    //根据异步结果改变Promise状态
    if(result === 3){
    //异步任务执行成功,调用resolve函数,同时改变Promise状态为fulfilled
    resolve('成功');
    }else{
    //执行失败,调用reject函数,同时改变Promise状态为rejected
    reject('失败');
    }
    });

    function success(text){
    console.log(text);
    }

    function fail(text){
    console.log(text);
    }
    //Promise.then方法是用来指定异步任务执行后的回调函数。
    //下面的代码中success方法和fail方法
    //但他们并不对应Promise实例创建时传入的resolve和reject方法。
    //只有当resolve方法调用后Promise状态发生改变,success方法才会被调用,fail方法同理。
    //两个参数顺序是固定的,不可交换。
    promise.then(success,fail);
  2. 使用要点

    在创建Promise对象时,执行resolve()或reject()函数并不会结束该函数的运行,因为它们会等当前函数中的同步任务完成后再执行。

    1
    2
    3
    4
    5
    6
    7
    new Promise((resolve, reject) => {
    resolve(1);
    console.log(2);
    }).then(r => {
    console.log(r);
    });
    //先输出2再输出1

    如果一个Promise对象中返回了另一个Promise对象,那么前一个对象的状态将由后一个对象的状态决定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var p1 = new Promise(function(resolve,reject){
    reject(new Error('p1 failed'));
    });

    var p2 = new Promise(function(resolve,reject){
    resolve(p1);
    //等价于p1.then(resolve).catch(reject);
    });
    //此时p2相当于p1
    p2.then(function(){
    console.log('p2 resolved');
    }).catch(function(){
    console.log('p2 failed');
    })
  3. then方法和catch方法

    then方法有2个参数,一个是fulfilled状态后的回调函数,一个是rejected状态后的回调函数。一旦当前js中的同步任务完成,Promise对象开始调用resolve/reject方法,并且从pending状态改变为resolved状态,此时then方法获得通知,调用相应的回调函数。

    catch方法与then方法本质相同,但只需要传入rejected状态的回调函数,相当于then(null,reject(){});

    这两个函数都可以捕获错误。如果有多个链式调用,Error会累积到第一个catch语句处理。

  4. all方法

    Promise.all([])方法接收一个Promise对象数组,如果数组项中存在不是Promise对象,则调用resolve()方法对它进行处理。

    1
    var all = Promise.all([p1,p2,p3,...,pn]);

    all方法的作用是将多个Promise的运行结果进行与运算,只有当数组中所有Promise的状态都是fulfilled状态,all这个Promise对象的状态才是fulfilled。而一旦其中有一个状态为rejected,all的状态就是rejected。

    此外,如果数组中的子项独自声明了catch或then方法,all的catch方法将不会被调用。因为子项在执行catch和then方法后会返回另一个新的Promise对象,状态为resolved。

  5. race方法

    race方法是将一组Promise对象竞速,谁的任务结果最先出来,就返回谁。

  6. resolve方法

    resolve方法作用是将传入的参数转换成Promise对象,新对象在执行resolve方法时传入的参数,也会传递给then方法中定义的回调函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var p = new Promise(function(resolve,reject){
    resolve(1);
    });

    Promise.resolve(p).then(function(a){
    //a===1
    //a!==p
    console.log(a);
    })

    有四种情况需要处理:

    1. Promise对象:如果传入的参数是Promise对象,则原样返回。
    2. thenable对象:thenable对象指的是具有then方法的对象。如:

      1
      2
      3
      4
      5
      var thenable = {
      then:function(resolve,reject){
      resolve();
      }
      }

      resolve方法会将该对象转换成Promise对象,并且立刻执行该对象的then方法。

    3. 其他类型:对于没有then方法的对象,或者根本不是对象的参数,resolve方法将返回一个新的Promise对象,并且该对象状态为fulfilled。
    4. 无参数:没有参数的resolve方法会返回一个新的Promise对象,且状态为fulfilled。
  7. reject方法
    reject方法会返回一个状态为rejected的Promise对象。与resolve方法有区别的地方,就是该方法的参数会传递给rejected对应的回调函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var p = new Promise(function(resolve,reject){
    reject(1);
    });

    Promise.reject(p).catch(function(a){
    //a!==1
    //a==p
    console.log(a);
    })
  8. 改写Ajax方法

    使用Promise对象改写Ajax方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function ajax(url){
    var promise = new Promise(function(resolve,reject){
    var xhr = new XMLHttpRequest();
    xhr.open('GET',url);
    xhr.send();
    xhr.onreadystatechange = function(){
    if(xhr.readyState === 4){
    if(xhr.status === 200){
    resolve(xhr.response);
    }else{
    reject(new Error('get failed'));
    }
    }
    };
    xhr.responseType = 'json';
    xhr.setRequestHeader('Accept','application/json');
    });
    return promise;
    }
    ajax('www.baidu.com').then(function(json){
    console.log("res:"+json);
    }).catch(function(err){
    console.log(err);
    });

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。