学习廖雪峰的JS教程提取笔记

  1. 学习浏览器的断开,单步执行等用法(相见恨晚、以后不用console.log()啦!)

  2. 最好不要一行写两个语句。

  3. 要特别注意相等运算符==。JavaScript在设计时,有两种比较运算符:

    第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;

    第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

    由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。

  4. 另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己:

    1
    NaN === NaN; // false

    唯一能判断NaN的方法是通过isNaN()函数:

    1
    isNaN(NaN); // true
  5. null和undefined

    null表示一个“空”的值,它和0以及空字符串''不同,0是一个数值,''表示长度为0的字符串,而null表示“空”。

    在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。

    JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。

  6. 对象里的键可以称为是对象的属性。可以直接用object.key

  7. 动态语言与静态语言

    这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言,赋值语句如下:

    1
    2
    int a = 123; // a是整数类型变量,类型用int申明
    a = "ABC"; // 错误:不能把字符串赋给整型变量

    和静态语言相比,动态语言更灵活,就是这个原因。

字符串、数组

  1. 多行字符串

    由于多行字符串用\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用反引号 `` 表示:

    1
    2
    3
    `这是一个
    多行
    字符串`;
  2. 如果有很多变量需要连接,用+号就比较麻烦。ES6新增了一种模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:

    1
    2
    3
    4
    var name = '小明';
    var age = 20;
    var message = `你好, ${name}, 你今年${age}岁了!`;
    alert(message);
  3. indexOf

    indexOf()会搜索指定字符串出现的位置:

    1
    2
    3
    var s = 'hello, world';
    s.indexOf('world'); // 返回7
    s.indexOf('World'); // 没有找到指定的子串,返回-1
  4. substring

    substring()返回指定索引区间的子串:

    1
    2
    3
    var s = 'hello, world'
    s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello'
    s.substring(7); // 从索引7开始到结束,返回'world'
  5. 请注意,如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化:

    1
    2
    3
    var arr = [1, 2, 3];
    arr[5] = 'x';
    arr; // arr变为[1, 2, 3, undefined, undefined, 'x']

    大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的Array却不会有任何错误。在编写代码时,不建议直接修改Array的大小,访问索引时要确保索引不会越界。

  6. slice

    slice()就是对应String的substring()版本,它截取Array的部分元素,然后返回一个新的Array

    1
    2
    3
    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()的起止参数包括开始索引,不包括结束索引。

    如果不给slice()传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个Array

    1
    2
    3
    4
    var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
    var aCopy = arr.slice();
    aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G']
    aCopy === arr; // false
  7. 数组的堆栈用法:

    • push和pop

      push()Array的末尾添加若干元素,pop()则把Array的最后一个元素删除掉:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      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

      如果要往Array的头部添加若干元素,使用unshift()方法,shift()方法则把Array的第一个元素删掉:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      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; // []
  8. sort排序

  9. Reverse反序

  10. splice

    splice()方法是修改Array的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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']
  11. concat

    concat()方法把当前的Array和另一个Array连接起来,并返回一个新的Array

    1
    2
    3
    4
    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

    实际上,concat()方法可以接收任意个元素和Array,并且自动把Array拆开,然后全部添加到新的Array里:

    1
    2
    var arr = ['A', 'B', 'C'];
    arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]
  12. join方法和ruby的类似

  13. 访问属性是通过.操作符完成的,但这要求属性名必须是一个有效的变量名。如果属性名包含特殊字符,就必须用''括起来:

    1
    2
    3
    4
    var xiaohong = {
    name: '小红',
    'middle-school': 'No.1 Middle School'
    };

    如果我们要检测xiaoming是否拥有某一属性,可以用in操作符:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var xiaoming = {
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70,
    weight: 65,
    score: null
    };
    'name' in xiaoming; // true
    'grade' in xiaoming; // false

    不过要小心,如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的:

    1
    'toString' in xiaoming; // true

    因为toString定义在object对象中,而所有对象最终都会在原型链上指向object,所以xiaoming也拥有toString属性。

    要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:

    1
    2
    3
    4
    5
    var xiaoming = {
    name: '小明'
    };
    xiaoming.hasOwnProperty('name'); // true
    xiaoming.hasOwnProperty('toString'); // false
  14. 可以省略{}在选择性语句中:var age = 20;if (age >= 18) { // 如果age >= 18为true,则执行if语句块 alert(‘adult’);} else { // 否则执行else语句块 alert(‘teenager’);}

    其中else语句是可选的。如果语句块只包含一条语句,那么可以省略{}

    1
    2
    3
    4
    5
    var age = 20;
    if (age >= 18)
    alert('adult');
    else
    alert('teenager');

    (但,不能超过一个语句!!!)

  15. JavaScript把nullundefined0NaN和空字符串''视为false,其他值一概视为true

  16. prompt可以直接通过浏览器提示框让用户输入!!!

    image-20180909213540561

  17. 循环For:

    JavaScript的循环有两种,一种是for循环,通过初始条件、循环条件和递增条件来循环执行语句块:

    1
    2
    3
    4
    5
    6
    var x = 0;
    var i;
    for (i=1; i<=10000; i++) {
    x = x + i;
    }
    x; // 50005000

    让我们来分析一下for循环的控制条件:

    • i=1 这是初始条件,将变量i置为1;

    • i<=10000 这是判断条件,满足时就继续循环,不满足就退出循环;

    • i++ 这是每次循环后的递增条件,由于每次循环后变量i都会加1,因此它终将在若干次循环后不满足判断条件i<=10000而退出循环。

`for`循环的3个条件都是可以省略的,如果没有退出循环的判断条件,就必须使用`break`语句退出循环,否则就是死循环:

1
2
3
4
5
6
7
var x = 0;
for (;;) { // 将无限循环下去
if (x > 100) {
break; // 通过if判断来退出循环
}
x ++;
}
  1. for … in

    for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来:

    1
    2
    3
    4
    5
    6
    7
    8
    var o = {
    name: 'Jack',
    age: 20,
    city: 'Beijing'
    };
    for (var key in o) {
    console.log(key); // 'name', 'age', 'city'
    }
  2. 由于Array也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in循环可以直接循环出Array的索引:

    1
    2
    3
    4
    5
    var a = ['A', 'B', 'C'];
    for (var i in a) {
    console.log(i); // '0', '1', '2'
    console.log(a[i]); // 'A', 'B', 'C'
    }

    请注意for ... inArray的循环得到的是String而不是Number

  3. while

    for循环在已知循环的初始和结束条件时非常有用。而上述忽略了条件的for循环容易让人看不清循环的逻辑,此时用while循环更佳。

    while循环只有一个判断条件,条件满足,就不断循环,条件不满足时则退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:

    1
    2
    3
    4
    5
    6
    7
    var x = 0;
    var n = 99;
    while (n > 0) {
    x = x + n;
    n = n - 2;
    }
    x; // 2500

    在循环内部变量n不断自减,直到变为-1时,不再满足while条件,循环退出。

  4. do … while

    最后一种循环是do { ... } while()循环,它和while循环的唯一区别在于,不是在每次循环开始的时候判断条件,而是在每次循环完成的时候判断条件:

    1
    2
    3
    4
    5
    var n = 0;
    do {
    n = n + 1;
    } while (n < 100);
    n; // 100

    do { ... } while()循环要小心,循环体会至少执行1次,而forwhile循环则可能一次都不执行。

5种循环方式:

  • for(起始条件;循环条件;递增条件)……
  • For….in…..
  • while……
  • Do……while…..
  • 数组的forEach()
  1. Map

    Map是一组键值对的结构,具有极快的查找速度

    举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array

    1
    2
    var names = ['Michael', 'Bob', 'Tracy'];
    var scores = [95, 75, 85];

    给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。

    如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用JavaScript写一个Map如下:

    1
    2
    var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
    m.get('Michael'); // 95

    初始化Map需要一个二维数组,或者直接初始化一个空MapMap具有以下方法:

    1
    2
    3
    4
    5
    6
    7
    var m = new Map(); // 空Map
    m.set('Adam', 67); // 添加新的key-value
    m.set('Bob', 59);
    m.has('Adam'); // 是否存在key 'Adam': true
    m.get('Adam'); // 67
    m.delete('Adam'); // 删除key 'Adam'
    m.get('Adam'); // undefined
  2. Set

    SetMap类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。

    要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set

    1
    2
    var s1 = new Set(); // 空Set
    var s2 = new Set([1, 2, 3]); // 含1, 2, 3

    重复元素在Set中自动被过滤:

    1
    2
    var s = new Set([1, 2, 3, 3, '3']);
    s; // Set {1, 2, 3, "3"}

    注意数字3和字符串'3'是不同的元素。

    通过add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果:

    1
    2
    3
    4
    s.add(4);
    s; // Set {1, 2, 3, 4}
    s.add(4);
    s; // 仍然是 Set {1, 2, 3, 4}

    通过delete(key)方法可以删除元素:

    1
    2
    3
    4
    var s = new Set([1, 2, 3]);
    s; // Set {1, 2, 3}
    s.delete(3);
    s; // Set {1, 2}

这两个有点像redis里的方法。

函数

  1. 匿名函数:function (x) { ... }

  2. 函数的传参数量没有严格限定。可以少也可以多个,在函数内部可以用arguments 把所有的参数当一个数组来调用。但这种方法在函数内部需要自己写语句来判断,显得有点别扭。新的做法是:…rest

    1
    2
    3
    4
    5
    function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
    }
  3. 变量提升

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
    }
    foo();
    相当于:
    function foo() {
    var y; // 提升变量y的申明,此时y为undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
    }

    所以:通常会在定义函数伊始就定义好所有的变量!

  4. 全局作用域

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

    1
    2
    3
    4
    5
    'use strict';
    var course = 'Learn JavaScript';
    alert(course); // 'Learn JavaScript'
    alert(window.course); // 'Learn JavaScript'

    因此,直接访问全局变量course和访问window.course是完全一样的。

  5. 名字空间

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

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

    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';
    };

    原来还有命名空间这么一说。

  6. 常量(这里把let 和 const讲得很接地气!)

    由于varlet申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:

    1
    var PI = 3.14;

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

    1
    2
    3
    4
    5
    'use strict';
    const PI = 3.14;
    PI = 3; // 某些浏览器不报错,但是无效果!
    PI; // 3.14
  7. 快速赋值和提取属性:

    1
    2
    3
    4
    5
    6
    7
    8
    var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
    };
    var {name, age, passport} = person;

    还可以有默认值:666

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
    };
    // 如果person对象没有single属性,默认赋值为true:
    var {name, single=true} = person;
    name; // '小明'
    single; // true

    对象的解构还是蛮方便的!直接用对象来解构

    如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。例如,下面的函数可以快速创建一个Date对象:

    1
    2
    3
    function buildDate({year, month, day, hour=0, minute=0, second=0}) {
    return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
    }

方法

  1. 在一个对象中绑定函数,称为这个对象的方法。

    在JavaScript中,对象的定义是这样的:

    1
    2
    3
    4
    var xiaoming = {
    name: '小明',
    birth: 1990
    };

    但是,如果我们给xiaoming绑定一个函数,就可以做更多的事情。比如,写个age()方法,返回xiaoming的年龄:

    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了
  2. this指的是调用者本身。(这里有坑需要注意!)

  3. apply

    虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefinedwindow,不过,我们还是可以控制this的指向的!

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

    apply修复getAge()调用:

    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, 参数为空

高阶函数

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

  1. map和reduce

    要把[1, 3, 5, 7, 9]变换成整数13579,reduce()也能派上用场:

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

    和rails基本无差,只是在调用的时候用法上有点小差别:

    1
    2
    3
    4
    5
    function pow(x) {
    return x * x;
    }
    var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    var results = arr.map(pow); //这里调用时,直接就自动传参啦,233
  2. filter函数:

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

    1
    2
    3
    4
    5
    var arr = ['A', '', 'B', null, undefined, 'C', ' '];
    var r = arr.filter(function (s) {
    return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
    });
    r; // ['A', 'B', 'C']

    把返回false的参数过滤掉!

  3. Sort函数
    排序算法

    排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个对象呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。通常规定,对于两个元素xy,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。

    JavaScript的Arraysort()方法就是用于排序的,但是排序结果可能让你大吃一惊:

    1
    2
    3
    4
    5
    6
    7
    8
    // 看上去正常的结果:
    ['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
    // apple排在了最后:
    ['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
    // 无法理解的结果:
    [10, 20, 1, 2].sort(); // [1, 10, 2, 20]

    js 的排序方法绝对是大坑!

    改进:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var arr = [10, 20, 1, 2];
    arr.sort(function (x, y) {
    if (x < y) {
    return -1;
    }
    if (x > y) {
    return 1;
    }
    return 0;
    });
    console.log(arr); // [1, 2, 10, 20]

    sort方法会直接改变数组本身!

箭头函数

1
x => x * x

上面的箭头函数相当于:

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

箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }return

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

如果参数不是一个,就需要用括号()括起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两个参数:
(x, y) => x * x + y * y
// 无参数:
() => 3.14
// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}

如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

1
2
// SyntaxError:
x => { foo: x }

因为和函数体的{ ... }有语法冲突,所以要改为:

1
2
// ok:
x => ({ foo: x })
重点!

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

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

1
2
3
4
5
6
7
8
9
10
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var 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
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25

如果使用箭头函数,以前的那种hack写法:

1
var that = this;

就不再需要了。万岁!

总结箭头函数:
  1. 简化
  2. 自动指定this
  3. 没有参数也需要用括号
  4. call和apply方法无法再用this来传数。

Generator函数

generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个好处要等到后面学了AJAX以后才能体会到。

没有generator之前的黑暗时代,用AJAX时需要这么写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ajax('http://url-1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});

回调越多,代码越难看。

有了generator的美好时代,用AJAX时可以这么写:

1
2
3
4
5
6
7
8
9
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}

点评:用法就是遇到yield的时候暂停,返回return一下yield后面返回的值,等函数再次调用时再运行下一步!

Date函数

关于时间的一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
now.getDate(); // 24, 表示24号
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳
var test = Date.now() //1536800166989
var d = new Date(1435146562875);
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地时间(北京时区+8:00),显示的字符串与操作系统设定的格式有关
d.toUTCString();
var d = new Date(2015, 5, 19, 20, 15, 30, 123);
d; // Fri Jun 19 2015 20:15:30 GMT+0800 (CST)
正则表达式
1
2
3
4
var re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false
1. 切分字符串

用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:

1
'a b c'.split(' '); // ['a', 'b', '', '', 'c']

嗯,无法识别连续的空格,用正则表达式试试:

1
'a b c'.split(/\s+/); // ['a', 'b', 'c']

这里原来有这样更好的用法!!!!

2. 分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:

^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

1
2
3
var re = /^(\d{3})-(\d{3,8})$/;
re.exec('010-12345'); // ['010-12345', '010', '12345']
re.exec('010 12345'); // null

如果正则表达式中定义了组,就可以在RegExp对象上用exec()方法提取出子串来。

exec()方法在匹配成功后,会返回一个Array,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。

exec()方法在匹配失败时返回null

3. 贪婪匹配

需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0

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

由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。

必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:

1
2
var re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']
4. 全局搜索

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

1
2
3
var r1 = /test/g;
// 等价于:
var 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
var s = 'JavaScript, VBScript, JScript and ECMAScript';
var re=/[a-zA-Z]+Script/g;
// 使用全局匹配:
re.exec(s); // ['JavaScript']
re.lastIndex; // 10
re.exec(s); // ['VBScript']
re.lastIndex; // 20
re.exec(s); // ['JScript']
re.lastIndex; // 29
re.exec(s); // ['ECMAScript']
re.lastIndex; // 44
re.exec(s); // null,直到结束仍没有匹配到

全局匹配类似搜索,因此不能使用/^...$/,那样只会最多匹配一次。

正则表达式还可以指定i标志,表示忽略大小写,m标志,表示执行多行匹配。

Json

1
{"name":"小明","age":14,"gender":true,"height":1.65,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}

要输出得好看一些,可以加上参数,按缩进输出:

1
JSON.stringify(xiaoming, null, ' ');

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "小明",
"age": 14,
"gender": true,
"height": 1.65,
"grade": null,
"middle-school": "\"W3C\" Middle School",
"skills": [
"JavaScript",
"Java",
"Python",
"Lisp"
]
}

序列化和反序列化:

1
2
JSON.stringify();
JSON.parse()

javascript面向对象

概念:
1
2
3
4
5
6
7
8
9
10
11
12
13
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
var xiaoming = {
name: '小明'
};
xiaoming.__proto__ = Student;

注意最后一行代码把xiaoming的原型指向了对象Student,看上去xiaoming仿佛是从Student继承下来的:

1
2
xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...

xiaoming有自己的name属性,但并没有定义run()方法。不过,由于小明是从Student继承而来,只要Studentrun()方法,xiaoming也可以调用:

xiaoming-prototype

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。

但通常不要这样直接__proto__来指定原型。

更好的作法:

Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原型对象:
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
function createStudent(name) {
// 基于Student原型创建一个新对象:
var s = Object.create(Student);
// 初始化新对象:
s.name = name;
return s;
}
var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
  1. 不要把原型链搞得太长

    函数也是一个对象,它的原型链是:

    1
    foo ----> Function.prototype ----> Object.prototype ----> null

    由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法。

    很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。

  2. 构造函数

    热泪盈眶,终于知道这个是怎么回事了!

    1
    2
    3
    4
    5
    6
    function Student(name) {
    this.name = name;
    this.hello = function () {
    alert('Hello, ' + this.name + '!');
    }
    }

    你会问,咦,这不是一个普通函数吗?

    这确实是一个普通函数,但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象:

    1
    2
    3
    var xiaoming = new Student('小明');
    xiaoming.name; // '小明'
    xiaoming.hello(); // Hello, 小明!

    new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:

    1
    2
    3
    4
    5
    6
    xiaoming.constructor === Student.prototype.constructor; // true
    Student.prototype.constructor === Student; // true
    Object.getPrototypeOf(xiaoming) === Student.prototype; // true
    xiaoming instanceof Student; // true

    注意:

    1
    2
    3
    var xiaohong = new Student();
    var xiaoliang = new Student();
    xiaohong.hello() === xiaoliang.hello(); // false

    这里两个hello函数是各自对象里的方法。所以如果new了很多对象,就会浪费很多。解法:

    1
    2
    3
    4
    5
    6
    7
    function Student(name) {
    this.name = name;
    }
    Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
    };

    这样,xiaoming, xiaoliang下次调用是到Student找到这个方法来调用!

  3. 不要忘记写new

    因为如果忘了写new, 在函数里很多属性会直接通过this增加到window,会更糟糕!So,一般构造函数通常会以大写字母开关。

    更好的做法是,自定一个函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Student(props) {
    this.name = props.name || '匿名'; // 默认值为'匿名'
    this.grade = props.grade || 1; // 默认值为1
    }
    Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
    };
    function createStudent(props) {
    return new Student(props || {})
    }

    这个createStudent()函数有几个巨大的优点:一是不需要new来调用,二是参数非常灵活,可以不传,也可以这么传:

    1
    2
    3
    4
    5
    var xiaoming = createStudent({
    name: '小明'
    });
    xiaoming.grade; // 1

    如果拿到的是json数据 ,还可以直接new对象。

重新认识APPLE函数

apply就是用来改变方法内部若出现的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, 参数为空

apply带两个参数:第一个参数是要塞给方法内部this的对象,第二个是传给方法的参数,需要数组形式。

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

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

原型继承

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
35
36
37
38
39
// PrimaryStudent构造函数:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 空函数F:
function F() {
}
// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;
// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
// 创建xiaoming:
var xiaoming = new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true
// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

js-proto-extend

更好的作法,封装一个函数:
1
2
3
4
5
6
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 实现原型继承链:
inherits(PrimaryStudent, Student);
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};

终于ES6自己也看不过去了,出了个class来简化这些过程:

1
2
3
4
5
6
7
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}

如果用新的class关键字来编写Student,可以这样写:

构造函数:
1
2
3
4
5
6
7
8
9
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
原型的继承:
1
2
3
4
5
6
7
8
9
10
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}

这里的super方法理解为调用父方法!

帅!!!

Promise

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

其中的resolve和reject函数作用:把promise状态切换为已完成或者失败,并把结果往外传递。

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally()方法是最后的回调,无论如何都会执行;

1
2
3
4
5
6
7
8
9
10
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});

这里的Promise.all相当于ruby中的线程join方法,等all中的所有方法都有回调后才往下执行。

1
2
3
4
5
6
7
8
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})

这里嵌套后,p2的状态会因为p1而改变。

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

1
2
3
4
5
6
7
8
9
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});

如何往Promise里的调用函数传参数?下面是个例子:

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
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});

错误处理:

1
2
3
4
5
6
7
try {
...
} catch (e) {
...
} finally {
...
}
抛出错误: throw
1
2
3
4
5
6
7
8
9
10
11
12
13
var r, n, s;
try {
s = prompt('请输入一个数字');
n = parseInt(s);
if (isNaN(n)) {
throw new Error('输入错误');
}
// 计算平方:
r = n * n;
console.log(n + ' * ' + n + ' = ' + r);
} catch (e) {
console.log('出错了:' + e);
}