Web开发——JavaScript基础

目录


参考

JS注释参考

三方件参考


日志

  • 2021年08月22日11:09:05 基于初版补充async/await内容

1、函数

1.1 变量

ES6声明变量的六种方法

  1. var 命令

  2. function 命令

  3. let 命令:声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。

  4. const 命令:允许声明一个不可变的常量,这个常量在定义域内总是可见的。

    const PI = 3.14;
    PI = 1; // 将会抛出一个错误,因为改变了常量的值
    
  5. import 命令

  6. class 命令

ES6变量结构赋值

var [a, b, c] = ["1", "2", "3"]; // a = '1', b = '2', c = '3'

// 使用默认值
var [a, b, c = '3'] = ["1", "2"]; // c = '3'
var [a, b, c = '3'] = ["1", "2", undefined]; // c = '3'
var [a, b, c = '3'] = ["1", "2", null]; // c = null

// 对象结构赋值
var user = {
    a: '1',
    b: '2',
    c: '3'
};
var { a, b, c } = user; // 按照属性进行解构
var { b, a, c } = user;


// 圆括号的使用
// 如果变量已经定义,此时再用结构就会出现问题,编译会报错,此时可以用圆括号解决该问题
var foo = 'Hello, world';
console.log(foo); // Hello, world
// { foo } = { foo: "Hi, how are you?" }; // 会报错
({ foo } = { foo: "Hi, how are you?" }); // 解决该报错
console.log(foo); // Hi, how are you?


// 字符串解构
var str = "Hello, world!";
var [a, b, c, d, e, f, g] = str; // a = 'H', b = 'e', c = 'l', d = 'l'

1.2 运算符

// 如果你用一个字符串加上一个数字(或其他值),那么操作数都会被首先转换为字符串
console.log("3" + 4 + 5); // 345
console.log(3 + "4" + 5); // 345
console.log(3 + 4 + "5"); // 75

// 由两个“=(等号)”组成的相等运算符有类型自适应的功能(即可以进行自动类型转换)
console.log(123 == '123'); // true
console.log(1 == true); // true

// 如果在比较前不需要自动类型转换,应该使用由三个“=(等号)”组成的相等运算符
console.log(123 === '123'); // false
console.log(1 === true); // false

1.3 控制结构(for...in, for...of)

JavaScript 的控制结构与其他类 C 语言类似。可以使用 if 和 else 来定义条件语句,还可以使用if、else if和else连接起来使用;
​JavaScript 支持 while 循环和 do-while 循环。前者适合常见的基本循环操作,如果需要循环体至少被执行一次则可以使用 do-while;
​JavaScript 的 for 循环与 C 和 Java 中的相同,使用时可以在一行代码中提供控制信息。

  • for...in: 在什么位置
  • for...of: 元素内容
// 常规 for 循环
var arr = ['a', 'b', 'c'];
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // a, b, c
}


// for ... in
// do something with object property
var arr = ['a', 'b', 'c'];
for (let i in arr) {
    console.log(i); // 0, 1, 2
}

var arr = ['a', 'b', 'c'];
for (let i in arr) {
    console.log(arr[i]); // 'a', 'b', 'c'
}

var obj = {
    a1: 1,
    a2: 2,
    a3: 3,
    a4: 4,
}
for (let item in obj) {
    console.log(item); // a1, a2, a3, a4
}

var obj = {
    a1: 1,
    a2: 2,
    a3: 3,
    a4: 4,
}
for (let item in obj) {
    console.log(obj[item]); // 1, 2, 3, 4
}

1.4 对象字面量

举例1:

// "对象字面量" 也可以用来在对象实例中定义一个对象
var obj = {
    name: "Carrot",
    "_for": "Max",
    details: {
        color: "orange",
        size: 12
    }
};
// 对象的属性可以通过链式(chain)表示方法进行访问:
console.log(obj.details.color); // orange
console.log(obj["details"]["color"]); // orange

// 下面的例子创建了一个对象原型,Person,和这个原型的实例,You。
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 定义一个对象
var You = new Person("John", 24);
console.log(You.name); // John
console.log(You.age); // 24

// 完成对象创建后,对象属性可以通过如下两种方式进行赋值和访问
// 方式一
obj.name = "Simon";
var name_str = obj.name;
// 方式二
obj['name'] = "Simon";
var name_str = obj['name'];

// can use a variable to define a key
// 方式一
var user1 = 'what is your key?';
obj[user1] = 'what is its value?';
console.log(JSON.stringify(obj)); // {"name":"Simon","_for":"Max","details":{"color":"orange","size":12},"what is your key?":"what is its value?"}
// 方式二
obj["user2"] = "user2_value";
console.log(JSON.stringify(obj)); // {"name":"Simon","_for":"Max","details":{"color":"orange","size":12},"what is your key?":"what is its value?","user2":"user2_value"}

举例2:

console.log("\n------------反射测试------------");
var flight = {
    airline: "Oceanic",
    number: 815,
    departure: {
        IATA: "SYD",
        time: "2018-09-30",
        city: "Sydney"
    },
    arrival: {
        IATA: "LAX",
        time: "2018-09-29",
        city: "Los Angeles"
    }
};
flight.equipment = {
    model: 'Boring 777'
};
flight.status = 'overdue';
console.log(flight);
typeof flight.number;           // 'number', typeof类似于Java中的instanceof
typeof flight.status;           // 'string'
typeof flight.arrival;          // 'object'
typeof flight.mainfest;         // 'undefined'

typeof flight.toString;         // 'function'
typeof flight.constructor;      // 'function'

console.log("flight.number: " + flight.number);
console.log("flight.hasOwnProperty('number'): " + flight.hasOwnProperty('number')); // true
console.log("flight.hasOwnProperty('function'): " + flight.hasOwnProperty('function')); // false
console.log("flight.hasOwnProperty('constructor'): " + flight.hasOwnProperty('constructor')); // false

console.log("\n------------枚举测试------------");
var body = {
    first_name: "First_Name",
    last_name: "Last_Name",
    'profession': "Profession"
};
body.middle_name = "Middle_Name";
console.log(JSON.stringify(body)); // {"first_name":"First_Name","last_name":"Last_Name","profession":"Profession","middle_name":"Middle_Name"}

var obj = {
    a: 1,
    b: 2,
    c: 3
};
console.log(JSON.stringify(obj)); // {"a":1,"b":2,"c":3}
delete obj.a;
console.log(JSON.stringify(obj)); // {"b":2,"c":3}

举例3(自定义对象):

function makePerson(firstName, lastName) {
    return {
        firstName: firstName,
        lastName: lastName,
        personFullName: function () {
            return this.firstName + ' ' + this.lastName;
        },
        personFullNameReserved: function () {
            return this.lastName + ', ' + this.firstName;
        }
    };
}

var se = makePerson("Simon", "Willison");
console.log(se.personFullName()); // Simon Willison
console.log(se.personFullNameReserved()); // Willison, Simon

举例4(自定义对象):

function Person(first, last) {
    this.first = first;
    this.last = last;
    this.personFullName = function () {
        return this.first + ' ' + this.last;
    };
    this.personFullNameReserved = function () {
        return this.last + ', ' + this.first;
    }
}

var s = new Person("Simon", "Willison");
console.log(s.personFullName()); // Simon Willison
console.log(s.personFullNameReserved()); // Willison, Simon

注意,含有this的特定个函数不会返回任何值,只会修改this对象本身。new关键字将生成的this对象返回给调用方,而被new调用的函数成为构造函数。习惯的做法是将这些函数的首字母大写,这样用new调用它们的时候就容易识别了。
不过这个改进的函数还是和上一个例子一样,单独调用fullName()时会产生相同的问题。
我们的Person对象现在已经相当完善了,但还有一些不太好的地方。每次我们创建一个Person对象的时候,我们都在其中创建了两个新的函数——如果过这个代码可以共享不是更好吗?

举例4(自定义对象(推荐)):

function Person(first, last) {
    this.first = first;
    this.last = last;
}

Person.prototype.fullName = function () {
    return this.first + ' ' + this.last;
}

Person.prototype.fullNameReserved = function () {
    return this.last + ', ' + this.first;
}

var s = new Person("Simon", "Willison");
console.log(s.fullName()); // Simon Willison
console.log(s.fullNameReserved()); // Willison, Simon

Person.prototype是一个可以被Person的所有实例共享的对象。它是一个原型链(prototype chain)的查询链的一部分:当你试图访问一个Person没有定义的属性时,解释器会首先检查这个Person.prototype来判断是否存在这个属性。所以,任何分配给Person.prototype的东西对通过this对象构造的实例都是可用的。
​这个特性功能十分强大,JavaScript允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时(runtime)给已经存在的对象添加额外的方法。

举例5:还可以给JavaScript的内置函数原型(prototype)添加东西。让我们给String添加一个方法用来返回逆序的字符串。

var str = "Simon";
// str.reserved(); // TypeError: str.reserved is not a function

String.prototype.reserved = function () {
    var r = '';
    for (let i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
}

console.log(str.reserved()); // nomiS

// 定义新变量也可以在字符串字面量上用(string literal)
console.log("This can now be reserved".reserved()); // devreser eb won nac sihT

​ 正如前面提到的,原型组成链的一部分。那条链的根节点是Object.prototype,它包括toString()方法——将对相关转换成字符串时调用的方法。这对于我们的Person对象很有用:

var s = new Person("Simon", "Willison");
Person.prototype.toString = function () {
    return '<Person: ' + this.fullName() + '>';
}
console.log(s.toString()); // <Person: Simon Willison>

1.5 函数字面量

函数对象可以通过函数字面量来创建。函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其它函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数与变量。通过函数字面量创建的函数对象包含一个连到外部上下文的连接。这被称为闭包(即类似于C的局部变量和全局变量的作用域情况)。它是JavaScript强大表现力的根基。

举例1:

var add = function (a, b) {
    return a + b;
};

function add() {
    // let声明一个块级作用域的本地变量,并且可选的将其初始化为一个值,当然可以用var替代let
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

var val = add(3, 4, 5, 6, 10);
console.log("val: " + val); // 输出28

// 接下来创建一个求取平均值的函数
function avg1() {
    // let声明一个块级作用域的本地变量,并且可选的将其初始化为一个值,当然可以用var替代let
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}
var val_avg1 = avg1(3, 4, 5, 6, 10);
console.log("val_avg1: " + val_avg1); // 5.6

// JavaScript允许创建匿名函数,这个函数在语义上与function avg()相同。
var avg2 = function () {
    // let声明一个块级作用域的本地变量,并且可选的将其初始化为一个值,当然可以用var替代let
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}

var val_avg2 = avg2(3, 4, 5, 6, 10);
console.log("val_avg2: " + val_avg2); // 5.6


// 但如果能重用我们已经创建的那个函数不是更好吗?幸运的是 JavaScript 允许使用任意函数对象的apply() 方法来调用该函数,并传递给它一个包含了参数的数组。传给 apply() 的第二个参数是一个数组,它将被当作 avg() 的参数使用,至于第一个参数 null,我们将在后面讨论。这也正说明一个事实——函数也是对象。
var val_avg2 = avg1.apply(null, [3, 4, 5, 6, 10]);
console.log("val_avg2: " + val_avg2); // 5.6

// 基于这个特性,有人发明出一些有趣的技巧。与 C 中的块级作用域类似,下面这个例子隐藏了局部变量
// JavaScript函数加括号表示执行该函数,不加括号仅仅表示定义了一个函数对象
// 即:函数立即执行,输出:Hello, world
(function test() {
    console.log("Hello, world");
})(); 

举例2(avg() 函数处理一个由逗号连接的变量串,但如果想得到一个数组的平均值该怎么办呢?可以这么修改函数):

function avgArray(arr) {
    // let声明一个块级作用域的本地变量,并且可选的将其初始化为一个值,当然可以用var替代let
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum / arr.length;
}
var val_avgArray = avgArray([3, 4, 5, 6, 10]);
console.log("val_avgArray: " + val_avgArray); // 5.6

1.6 函数调用

每个函数接收两个附加的参数:this和argument。

当实际参数(arguments)的个数与形式参数(parameters)的个数不匹配时不会导致运行时错误。如果实际参数过多了,超出的参数值将被忽略。如果实际参数过少,缺失的值将会被替换为undefined。对参数值不会进行类型检查:任何类型的值都可以被传递给参数。

  1. 方法调用模式;
  2. 函数调用模式;
  3. 构造器调用模式(如果调用构造器函数时候没有在前面加上new,可能会发生非常糟糕的事情,既没有编译时警告,也没有运行时警告,所以大写约定非常重要);
  4. apply调用模式。

举例:

// 因为函数是对象,所以它们可以像任何其它的值一样被使用。函数可以存放在变量、对象和数组中
// 函数可以被当作参数传递给其它函数,函数也可以再返回函数
// 因为函数是对象,所以函数可以拥有方法
console.log("\n------------原型测试------------");
function person() {
    person.prototype.name = "Jingzi";
    person.prototype.age = 20;
    person.prototype.sayName = function () {
        console.log(this.name);
    }
}
var person_1 = new person();
person_1.sayName();

console.log("\n------------函数字面量测试------------");
// 认为是把一个匿名函数存放在一个add变量中
var add = function (a, b) {
    return a + b;
};
console.log(add("Hello, ", "world!"));

console.log("\n------------1.1 方法调用模式------------");
// 创建myObject。它有一个value属性和一个increment方法
// increment方法可以接受一个可选的参数。如果参数不是数字,那么默认使用数字1。
var myObject = {
    value: 0,
    increment: function (inc) {
        this.value += typeof inc === 'number' ? inc : 1;
    }
};

myObject.increment();
console.log(myObject.value);    // 1
// document.writeln(myObject.value);
myObject.increment(2);
console.log(myObject.value);    // 3
// document.writeln(myObject.value);

console.log("\n------------1.2 函数调用模式------------");
var sum = add(3, 4);            // 7
console.log(sum);
// 给myObject增加一个double方法
myObject.double = function () {
    var that = this;            // 解决方法
    var helper = function () {
        that.value = add(that.value, that.value);
    }
    helper();                   // 以函数的形式调用helper
    console.log(that);
};
// 以方法 的形式调用double
myObject.double();

console.log("\n------------1.3 构造器调用模式------------");
// 创造一个名为Quo的构造器函数。它构造一个带有status属性的对象
var Quo = function (str) {
    this.status = str;
};
// 给Quo的所有实例提供一个名为get_status的公共方法
Quo.prototype.get_status = function () {
    return this.status;
};
// 构造一个Quo实例
var myQuo = new Quo("confused");
console.log(myQuo.get_status());

// 因为JavaScript时一门函数式的面向对象编程语言,所以函数可以拥有方法
// 1.3中的构造器函数Quo和为Quo实例的公共方法也是必需的
console.log("\n------------1.4 Apply调用模式------------");
// 构造一个包含两个数字的数组,并将它们相加
var array = [3, 4];
var sum = add.apply(null, array);          // sum值为7
console.log(sum);
// 构造一个包含status成员的对象
var statusObject = {
    status: 'A-OK'
};
var status = Quo.prototype.get_status.apply(statusObject);
console.log(status);

1.7 函数参数

当函数被调用时,会得到一个“免费”奉送的参数,那就是arguments数组。通过它函数可以访问所有它被调用时传递给它的参数列表,包括那些没有被分配给函数声明时定义的形式参数的多余参数。这使得编写一个无须指定参数个数的函数成为可能。

举例1(自带的arguments参数):

// 构造一个将很多个值相加的函数
var sumFunc = function () {
    var i, sum = 0;
    for (i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
};
console.log(sumFunc(4, 7, 15, 16, 23, 42)); // 107
// document.writeln(sum(4, 7, 15, 16, 23, 42));

举例2(rest参数):

// 构造一个将很多个值相加的函数
var sumFunc = function (...values) {
    var sum = 0;
    for (item of values) {
        sum += item;
    }
    return sum;
};
console.log(sumFunc(4, 7, 15, 16, 23, 42)); // 107
// document.writeln(sum(4, 7, 15, 16, 23, 42));

const sortNumber = (...numbers) => numbers.sort();
sortNumber(1, 2, 3, 4, 6, 7, 32, 1, 2); // [1, 1, 2, 2, 3, 32, 4, 6, 7]

1.8 函数异常

如果在try代码块内抛出了一个异常,控制权就会跳转到它的catch从句。
一个try语句只会有一个将捕获所有异常 的catch代码块。如果你的处理手段取决于异常的类型,那么异常处理器必须检查异常对象的name属性以确定异常的类型。

举例:

// throw语句中断函数的执行。它应该抛出一个exception异常
// 该对象包含可识别异常类型的name属性和一个描述性的message属性,当然也可以添加其它的属性。
var add = function (a, b) {
    if ((typeof a != 'number') || (typeof a != 'number')) {
        throw {
            name: 'TypeError',
            message: 'add needs numbers'
        }
    }
    return a + b;
};

// 该exception对象被传递到一个try语句的catch从句
var try_it = function () {
    try {
        add("seven");
    } catch (e) {
        console.log(e.name + ": " + e.message);
    }
}
try_it(); // TypeError: add needs numbers

1.9 函数——给类型增加方法

举例:

// JavaScript允许给语言的基本类型增加方法。举例来说,我们可以通过给Function.prototype增加方法来使得该方法对所有函数可用
// 通过给基本类型增加方法,我们可以大大提高语言的表现力。因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创建之前就创建好了
// 有条件地增加一个方法
Function.prototype.method = function (name, func) {
    if (!this.prototype[name]) {
        this.prototype[name] = func;
    }
};

// 通过Function.prototype增加一个method方法,我们就不必键入prototype这个属性名。这个缺点也就被掩盖了(下面代码运行还有问题,需要后续进行调试)
// 给类型增加方法
// JavaScript并没有单独的整数类型,因此有时候只提取数字中的整数部分是必要的。
// JavaScript本身提供的取整方法有些丑陋。我们可以通过给Number.prototype添加一个
// integer方法来改善它。它会根据数字的正负来判断是使用Math.ceiling还是Math.floor。
Number.method('integer', function () {
    return Math[this < 0 ? 'ceil' : 'floor'](this);
});
console.log((-10 / 3).integer());

1.10 函数——递归

“汉诺塔”是一个著名的难题。塔的设备包括三根柱子和一套直径各不相同的空心圆盘。开始时源柱子上的所有圆盘都按照较小的圆盘放在较大的圆盘之上的顺序堆叠。目标是通过每次移动一个圆盘到另一根柱子,最终将一对圆盘移动到目标柱子上,过程中不可以将大的圆盘放置在较小的圆盘之上。寻常解答如下:

var hanoi = function (disc, src, aux, dst) {
    if (disc > 0) {
        hanoi(disc - 1, src, dst, aux);
        console.log('Move disc ' + disc + ' from ' + src + ' to ' + dst);
        hanoi(disc - 1, src, aux, dst);
    }
};

hanoi(3, 'Src', 'Aux', 'Dst');

// Move disc 1 from Src to Dst
// Move disc 2 from Src to Aux
// Move disc 1 from Src to Aux
// Move disc 3 from Src to Dst
// Move disc 1 from Src to Aux
// Move disc 2 from Src to Dst
// Move disc 1 from Src to Dst

1.11 函数——闭包

举例1:

function makeAddr(a) {
    return function (b) {
        return a + b;
    };
}
var x = makeAddr(5);
var y = makeAddr(20);
console.log(x(6)); // 返回11
console.log(y(7)); // 返回27

makeAddr这个名字本身应该能说明函数是用来做什么的:它创建了一个新的adder函数,这个函数自身带有一个参数,它被调用的时候这个参数会被夹在外层函数传进来的参数上。

这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数已经返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则 adder 函数将不能工作。也就是说,这里存在 makeAdder 的局部变量的两个不同的“副本”——一个是 a 等于5,另一个是 a 等于20。那些函数的运行结果就如下所示:

下面来说说到底发生了什么。每当JavaScript执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它和被传入函数的变量一起被初始化。它和被传入函数的变量一起被初始化。这与那些保存的所有全局变量和函数的全局对象(global object)类似,但仍有一些很重要的区别,第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(在浏览器里面是当做 window 对象来访问的)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前的作用域对象里面属性的方法。

所以当调用 makeAdder 时,解释器创建了一个作用域对象,它带有一个属性:a,这个属性被当作参数传入 makeAdder 函数。然后 makeAdder 返回一个新创建的函数。通常 JavaScript 的垃圾回收器会在这时回收 makeAdder 创建的作用域对象,但是返回的函数却保留一个指向那个作用域对象的引用。结果是这个作用域对象不会被垃圾回收器回收,直到指向 makeAdder 返回的那个函数对象的引用计数为零。

作用域对象组成了一个名为作用域链(scope chain)的链。它类似于原型(prototype)链一样,被JavaScript的对象系统使用。

一个闭包就是一个函数和被创建的函数中的作用域对象的组合。

举例2:

function sayHello(name) {
    var text = 'Hello ' + name;
    var say = function () {
        console.log(text);
    };
    say();
}

sayHello("Joe"); // Hello Joe


function sayHello2(name) {
    var text = 'Hello ' + name;
    var say = function () {
        console.log(text);
    };
    return say;
}

var say2 = sayHello2("Joe");
say2(); // Hello Joe


function sayHello3(name) {
    var text = 'Hello ' + name;
    var say = function () {
        console.log(text);
    };
    return say();
}

var say3 = sayHello3("Joe");
say3; // Hello Joe

举例3(有权访问另一个函数作用域内变量的函数都是闭包):

function a() {
    var n = 0;
    function inc() {
        n++;
        console.log("n: " + n);
    }
    return inc;
}

var c = a();
c(); // 1
c(); // 2
c(); // 3

举例4:

var quo = function (status) {
    return {
        get_status: function () {
            return status;
        }
    }
};
// 创造一个Quo实例
var myQuo = quo("amazed");
console.log(myQuo.get_status()); // amazed


// 上述函数等价于:
var quo2 = function(status) {
    return {
        get_status: status
    };
};
var myQuo2 = quo2("amazed");
console.log(myQuo2.get_status);


// 上述函数亦等价于:
var quo3 = function(status) {
    return status;
};
var myQuo3 = quo3("amazed");
console.log(myQuo3);

1.12 函数——模块

可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量的使用,从而缓解这个JavaScript的最为糟糕的特性之一所带来的影响。

// 模块模式也可以用来产生安全的对象
// 假设我们想要构造一个用来产生序列号的对象
var serial_maker = function () {
    // 返回一个用来产生唯一字符串的对象
    // 唯一字符串由两个部分组成:前缀+序列号
    // 该对象包含一个设置前缀的方法,一个设置序列号的方法
    // 和一个产生唯一字符串的gensym方法
    var prefix = '';
    var seq = 0;
    return {
        set_prefix: function (p) {
            prefix = String(p);
        },
        set_seq: function (s) {
            seq = s;
        },
        gensym: function () {
            var result = prefix + '_' + seq;
            seq += 1;
            return result;
        }
    };
};

var seqer = serial_maker();
seqer.set_prefix('ZYJ');
seqer.set_seq('serialTest20181003');
var test_seqer = seqer.gensym();
console.log(test_seqer); // ZYJ_serialTest20181003

1.13 函数——记忆

函数可以调用对象去记忆先前操作的结果,从而避免无谓的运算。这种优化被称为记忆(memoization)。

比如说,我们想要一个递归函数计算Fibonacci数列。一个Fibonacci数字是之前两个Fibonacci数字之和。最前面两个数字是0和1。

var fibonacci = function (n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};

for (var i = 0; i <= 10; i++) {
    console.log('//' + i + ": " + fibonacci(i));
}

// 0: 0
// 1: 1
// 2: 1
// 3: 2
// 4: 3
// 5: 5
// 6: 8
// 7: 13
// 8: 21
// 9: 34
// 10: 55

这样是可以工作的,但它做了很多无谓的工作。fibonacci函数被调用了453次。我们调用了11次,而它自身调用了442次去计算可能已被刚刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少它的运算量。

我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道存储结果,如果已经知道,就立即返回这个存储结果。

var fibonacci = function () {
    var memo = [0, 1];
    var fib = function (n) {
        var result = memo[n];
        if (typeof result !== 'number') {
            console.log(n);
            result = fib(n - 1) + fib(n - 2);
            memo[n] = result;
        }
        return result;
    };
    return fib;
}();

for (var i = 0; i <= 10; i++) {
    console.log('//' + i + ": " + fibonacci(i));
}

// 0: 0
// 1: 1
// 2: 1
// 3: 2
// 4: 3
// 5: 5
// 6: 8
// 7: 13
// 8: 21
// 9: 34
// 10: 55

上述代码可以看出,这个函数返回同样的结果,但它只被调用了29次。我们调用了它11次,它自身调用了18次去取得之前存储的结果。

我们可以把这种形式一般化,编写一个函数来帮助我们构造带记忆功能的函数。

1.14 函数——内部函数

JavaScript 允许在一个函数内部定义函数,这一点我们在之前的 makePerson() 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是它们可以访问父函数作用域中的变量。

举例:

function betterExampledNeeded() {
    var a = 1;
    function oneMoreThanA() {
        return a + 1;
    }
    return oneMoreThanA();
}

如果某个函数依赖于其它的一两个函数,而这一两个函数对你其余的代码没有用处,你可以用它们嵌套在会被调用的那个函数内部,这样做可以减少全局作用域下的函数的数量,这有利于编写易于维护的 代码。

这也是一个减少使用全局变量的好方法。当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,订单这样做会使得代码很难维护。内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地方防止“污染”你的全局命名空间——可以称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。

1.15 函数——内存泄漏

使用闭包的一个坏处是,在 IE 浏览器中它会很容易导致内存泄露。JavaScript 是一种具有垃圾回收机制的语言——对象在被创建的时候分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。宿主环境提供的对象都是按照这种方法被处理的。

浏览器主机需要处理大量的对象来描绘一个正在被展现的 HTML 页面——DOM 对象。浏览器负责管理它们的内存分配和回收。

IE 浏览器有自己的一套垃圾回收机制,这套机制与 JavaScript 提供的垃圾回收机制进行交互时,可能会发生内存泄露。

在 IE 中,每当在一个 JavaScript 对象和一个本地对象之间形成循环引用时,就会发生内存泄露。如下所示:

function leakMemory() {
    var el = document.getElementById('e1');
    var o = {
        'el': el
    };
    el.o = o;
}

这段代码的循环引用会导致内存泄露:IE 不会释放被 el 和 o 使用的内存,直到浏览器被彻底关闭并重启后。

这个例子往往无法引起人们的重视:一般只会在长时间运行的应用程序中,或者因为巨大的数据量和循环中导致内存泄露发生时,内存泄露才会引起注意。

不过一般也很少发生如此明显的内存泄露现象——通常泄露的数据结构有多层的引用(references),往往掩盖了循环引用的情况。

闭包很容易发生无意识的内存泄露。如下所示:

function addHandler() {
    var el = document.getElementById('e1');
    el.onclick = function () {
        el.style.backgroundColor = 'red';
    };
}

这段代码创建了一个元素,当它被点击的时候变红,但同时它也会发生内存泄露。为什么?因为对 el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用。

这个问题有很多种解决方法,最简单的一种是不要使用 el 变量:

function addHandler() {
    document.getElementById('el').onclick = function() {
        this.style.backgroundColor = 'red';
    };
}

另外一种避免闭包的好方法是在 window.onunload 事件发生期间破坏循环引用。很多事件库都能完成这项工作。注意这样做将使 Firefox 中的 bfcache 无法工作。所以除非有其他必要的原因,最好不要在 Firefox 中注册一个onunload 的监听器。

1.16 函数——ES6函数扩展

1)箭头函数 (=>) 以及作用域

/* 基础语法 */
(参数1, 参数2, …, 参数N) => { 函数声明 }

//相当于:(参数1, 参数2, …, 参数N) =>{ return 表达式; }
(参数1, 参数2, …, 参数N) => 表达式(单一)

// 当只有一个参数时,圆括号是可选的:
(单一参数) => {函数声明}
单一参数 => {函数声明}

// 没有参数的函数应该写成一对圆括号。
() => {函数声明}


/* 高级语法 */
//加括号的函数体返回对象字面表达式:
参数=> ({foo: bar})

//支持剩余参数和默认参数
(参数1, 参数2, ...rest) => {函数声明}
(参数1 = 默认值1,参数2, …, 参数N = 默认值N) => {函数声明}

//同样支持参数列表解构
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f();  // 6

举例:

var testFunc1 = () => console.log("hello");
var testFunc2 = (x) => console.log("x: " + x);
var testFunc3 = (x, y) => console.log("x + y: " + (x + y));

testFunc1();
testFunc2("Hello, 2");
testFunc3(3, 5);


var func1 = ({value1, value2}) => ({total: value1 + value2})
var result = func1({value1: 2, value2: 3})
console.log("result: " + result);

函数中this作用域

var div = "xxx";
div.onclick = function () {
    setInterval(function () {
        this.style.backgroundColor = `xxx`; // 此时 this 指向的是 window
    }, 1000);
}

// 修改方式一:
var div = "xxx";
div.onclick = function () {
    let that = this;
    setInterval(function () {
        that.style.backgroundColor = `xxx`; // 此时 this 指向的是 window
    }, 1000);
}

// 修改方式二:
var div = "xxx";
div.onclick = function () {
    setInterval(() => {
        this.style.backgroundColor = `xxx`; // 此时 this 指向的是 window
    }, 1000);
}

2)参数默认值

function add(a = 1, b = 3) {
    return a + b;
}

add(); // 4
add(3, 4); // 7

3)扩展运算符

(1)扩展运算符可以用于复制数组

var arr1 = [1, 2, 3, 4, 6];
var arr2 = [...arr1];
console.log(arr1 === arr2); // false
console.log(JSON.stringify(arr1)); // [1, 2, 3, 4, 6]
console.log(JSON.stringify(arr2)); // [1, 2, 3, 4, 6]

(2)扩展运算符可以用于函数传参

// 扩展运算符,函数传参
function fn(a, b, c, d, e) {
    console.log(a);
    console.log(b);
    console.log(c);
    console.log(d);
    console.log(e);
}

var arr = ['1', '2', '3', '4', '5'];
fn(1, 2, 3, 4, 5, 6); // 1, 2, 3, 4, 5, 6
fn(...arr); // 1, 2, 3, 4, 5, 6

4)rest参数

// rest参数,即剩余参数
// 在函数声明中使用
function fn(a, b, ...args) {
    console.log(a);
    console.log(b);
    console.log(args);
    console.log(args[0]);
}

fn(1, 2, 3, 4, 5, 6); // 1, 2, [3, 4, 5, 6]

5)this绑定

6)尾调用

2、数组

2.1 数组字面量

一个数组字面量是在一对方括号中包围零个或多个用逗号分隔的值的表达式。数组字面量可以出现在任何表达式可以出现的地方。数组的第一个值将获得属性名'0',第二个值将获得属性名'1',依次类推:

// numbers继承来自Array.prototype,所以numbers继承了大量有用的方法。
// 同时numbers也有一个诡异的lenght属相,而numbers_object则(见下文中)没有
var empty = [];
var numbers = [
    'zero', 'one', 'two', 'three', 'four',
    'five', 'six', 'seven', 'eight', 'nine'
];

console.log('1.1 ' + empty[1]); // undefined
console.log('1.2 ' + numbers[1]); // 'one'
console.log('1.3 ' + empty.length); // 0
console.log('1.4 ' + numbers.length); // 10

// 对象字面量
// numbers_object继承来自Object.prototype
var numbers_object = {
    '0': 'zero', '1': 'one', '2': 'two', '3': 'three', '4': 'four',
    '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine'
};

// 在大多数语言中一个数组的所有元素都要求是相同的类型。
// JavaScript允许数组包含任意混合类型的值。
var misc = [
    'str', 98.6, true, false, null, undefined,
    ['nested', 'array'], { object: true }, NaN, Infinity
];
console.log('2.1 ' + misc.length); // 10

2.2 数组的长度

每个数组都有一个length属性。和大多数其它语言不同,JavaScript数组的length是没有上界的。如果你用大于或等于当前length的数字作为下标来保存一个元素,那么length将增大来容纳新元素。不会发生数组边界错误。

var myArray = [];
console.log("1 " + myArray.length);     // 0

myArray[10000] = true;
console.log("2 " + myArray.length);     // 10001, myArray数组只包含一个属性

// 可以直接设置length的值。
// 设置大的length无须给数组分配更多的空间。
// 而把length设小,将导致所有虾苗大于等于新length的属性被删除
var numbers = [
    'zero', 'one', 'two', 'three', 'four',
    'five', 'six', 'seven', 'eight', 'nine'
];
numbers.length = 3;
console.log('3.1 ' + numbers); // zero,one,two
// 附加一个新元素到该数组的尾部
numbers[numbers.length] = 'ten';
console.log('3.2 ' + numbers); // zero,one,two,ten
// 有时用push方法可以更方便地完成同样的事情
numbers.push('go');
console.log('3.3 ' + numbers); // zero,one,two,ten,go

2.3 数组的删除和查询

// 由于JavaScript的数组其实就是对象,
// 所以delete云算法可以用来从数组中移出元素
// 不幸的是,那样会在数组中遗留一个空洞
var numbers = [
    'zero', 'one', 'two', 'three', 'four',
    'five', 'six', 'seven', 'eight', 'nine'
];
delete numbers[2]; // ["zero", "one", empty, "three", "four", "five", "six", "seven", "eight", "nine"]
console.log(numbers);

// 幸运的是JavaScript数组有一个splice方法。
// 它可以对数组做个手术,删除一些元素并将它们替换为其它的元素
// 第一个参数是数组中的一个序号,第二个参数是要删除的元素个数
numbers.splice(2, 1);
console.log(numbers); // ["zero", "one", "three", "four", "five", "six", "seven", "eight", "nine"]

// 因为JavaScript的数组其实就是对象,所以for in语句可以用来遍历一个数组的所有属性。不幸的是,for in无法保证属性的顺序,而大多数的数组应用都期望按照阿拉伯数字顺序来产生元素。因此可以用常规的for语句进行查询使用
for (var i = 0; i < numbers.length; i++) {
    console.log(numbers[i]); // "zero", "one", "three", "four", "five", "six", "seven", "eight", "nine"
}

2.4 数组的方法

JavaScript提供了一套作用于数组的方法,这些方法是被存储在Array.prototype中的函数。前面有看到Object.prototype是可以扩充的。同样,Array.prototype也可以被扩充。

// 自己创建的Array.dim方法
Array.dim = function (dimension, initial) {
    var a = [];
    for (var i = 0; i < dimension; i++) {
        a[i] = initial;
    }
    return a;
};
// 创建一个包含10个0的数组
var myArray = Array.dim(10, 0);
console.log('1.1: ' + myArray);


// JavaScript没有多维数组,但就像大多数类C语言一样,它支持元素为数组的数组
var matrix = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8]
];
console.log(matrix[2][1]); // 7
console.log(matrix); // 0,1,2,3,4,5,6,7,8

// 修改Array.dim方法为Array.matrix
Array.matrix = function (m, n, initial) {
    var a;
    var mat = [];
    for (var i = 0; i < m; i++) {
        a = [];
        for (var j = 0; j < n; j++) {
            a[j] = initial;
        }
        mat[i] = a;
    }
    return mat;
};

// 构造一个用0填充的4*4矩阵
var myMatrix = Array.matrix(4, 4, 0);
console.log('3.1: ' + myMatrix[3][3]); // 0
console.log('3.2: ' + myMatrix); // 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

// 自己创建一个用来构造一个恒等矩阵的方法
Array.identity = function (n) {
    var mat = Array.matrix(n, n, 0);
    for (var i = 0; i < n; i++) {
        mat[i][i] = 1;
    }
    return mat;
};
myMatrix = Array.identity(4);
console.log('4.1: ' + myMatrix[3][3]); // 1
console.log('4.2: ' + myMatrix); // 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1

3、方法(标准类型上的标准方法,undefined, null, Bollean, String, Number, Object, Symbol)

JavaScript 是一种面向对象的动态语言,它包含类型、运算符、标准内置( built-in)对象和方法。它的语法来源于 Java 和 C,所以这两种语言的许多语法特性同样适用于 JavaScript。需要注意的一个主要区别是 JavaScript 不支持类,类这一概念在 JavaScript 通过对象原型(object prototype)得到延续(有关 ES6 类的内容参考这里Classes)。另一个主要区别是 JavaScript 中的函数也是对象,JavaScript 允许函数在包含可执行代码的同时,能像其他对象一样被传递。

3.1 Array

1)基本使用

方法名称 描述
a.toString() 返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。
a.toLocaleString() 根据宿主环境的区域设置,返回一个包含数组中所有元素的字符串,每个元素通过逗号分隔。
a.concat(item1[, item2[, ...[, itemN]]]) 返回一个数组,这个数组包含原先 a 和 item1、item2、……、itemN 中的所有元素。
a.join(sep) 返回一个包含数组中所有元素的字符串,每个元素通过指定的 sep 分隔。
a.pop() 删除并返回数组中的最后一个元素。
a.push(item1, ..., itemN) 将 item1、item2、……、itemN 追加至数组 a。返回数组a的长度。
a.reverse() 数组逆序(会更改原数组 a)。
a.shift() 删除并返回数组中第一个元素。
a.slice(start, end) 返回子数组,以 a[start] 开头,以 a[end] 前一个元素结尾。前闭后开。
a.sort([cmpfn]) 依据 cmpfn 返回的结果进行排序,如果未指定比较函数则按字符顺序比较(即使元素是数字)。
a.splice(start, delcount[, item1[, ...[, itemN]]]) 从 start 开始,删除 delcount 个元素,然后插入所有的 item。
a.unshift([item]) 将 item 插入数组头部,返回数组新长度(考虑 undefined)。

举例:

// array.concat(item...)
// concat方法返回一个新数组,它包含array的浅复制(shallow copy),
// 并将1个或多个参数与item附加在其后。
// 如果item是一个数组,那么它的每个元素都会被分别添加
var a = ['a', 'b', 'c'];
var b = ['x', 'y', 'z'];
var c = ['1', '2', '3'];
var d1 = a.concat(b, true);
console.log(d1); // ["a", "b", "c", "x", "y", "z", true]
var d2 = a.concat(b, false);
console.log(d2); // ["a", "b", "c", "x", "y", "z", false]
var d3 = a.concat(b, c, true);
console.log(d3); // ["a", "b", "c", "x", "y", "z", "1", "2", "3", true]
console.log("----------------1 array.concat(item...) end----------------");

// array.join(separator)
// join方法是把一个array构造成一个字符串。
// 它将array中的每个元素构造成一个字符串
// 并用一个separator为分隔符把它们连接在一起
// 默认的separator是','。
var a = ['a', 'b', 'c'];
a.push('d');
var c = a.join(',');
console.log(a); // ["a", "b", "c", "d"]
console.log(c); // a,b,c,d
console.log("----------------2 array.join(separator) end----------------");

// array.pop()
// pop和push方法使数组array像堆栈(stack)一样工作
// pop方法移除array中最后一个元素并返回该元素。如果该array为空,它会返回undefined
var a = ['a', 'b', 'c'];
var c = a.pop();
console.log(a); // ["a", "b"]
console.log(c); // c
console.log("----------------3 array.pop() end----------------");
// array.push(item...)
// push方法将一个或多个item附加到一个数组的尾部
// 不像concat方法那样,它会修改数组array,如果item是一个数组,
// 它会将参数数组作为单个元素整个添加到数组中。它返回这个数组array的新长度值
var a = ['a', 'b', 'c'];
var b = ['x', 'y', 'z'];
var c1 = a.push(b, true);
var c2 = a.push(b, false);
console.log(a); // ["a", "b", "c", Array(3), true, Array(3), false]
console.log(c1); // 5
console.log(c2); // 7
console.log("----------------4 array.push(item...) end----------------");

// array.reverse()
// 反转array中的元素的顺序
var a = ['a', 'b', 'c'];
var b = a.reverse();
console.log(a); // ["c", "b", "a"]
console.log(b); // ["c", "b", "a"]
console.log("----------------5 array.reverse() end----------------");

// array.shift()
// shift方法移除数组array中的第一个元素并返回该元素
var a = ['a', 'b', 'c'];
var b = a.shift();
console.log(a); // ["b", "c"]
console.log(b); // a
console.log("----------------6 array.shift() end----------------");

// array.slice(start, end),前开后闭,end参数是可选的
// slice方法对array中的一段做浅复制
// 如果start大于等于array.length,得到的结果将是一个空数组
var a = ['a', 'b', 'c'];
var b = a.slice(0, 1);
var c = a.slice(1);
var d = a.slice(1, 2);
console.log(a); // ["a", "b", "c"]
console.log(b); // ["a"]
console.log(c); // ["b", "c"]
console.log(d); // ["b"]
console.log("----------------7 array.slice() end----------------");

// array.spice(start, deleteCount, item...)
// splice方法从array中移除1个或多个元素,并用新的item替换它们,
// deleteCount是要移除的元素个数,它返回一个包含被移除元素的数组
var a = ['a', 'b', 'c'];
var b = a.splice(1, 1, 'ache', 'bug');
console.log(a); // ['a', 'ache', 'bug', 'c']
console.log(b); // ["b"]
console.log("----------------8 array.spice() end----------------");

// array.sort(comparefn)
// sort方法对array中的内容进行适当的排序。
// 它不能正确地给一组数字排序
var a = ['a', 'c', 'b'];
var n = [4, 5, 15, 16, 23, 42];
a.sort();
n.sort();
console.log(a); // ["a", "b", "c"]
console.log(n); // [15, 16, 23, 4, 42, 5]
console.log("----------------9 array.sort(comparefn) end----------------");

// array.unshift(item...)
// unshift方法像push方法一样用于将元素添加到数组中
// 但它是把item插入到array的开始部分而不是尾部。
// 它返回array的新的长度值
var a = ['a', 'b', 'c'];
var b = ['x', 'y', 'z'];
var c1 = a.push(b);
console.log(a); // ["a", "b", "c", Array(3)]
console.log(c1); // 4
console.log("----------------10 array.unshift(item...) end----------------");

2).forEach, .map(), .reduce(), .filter(), .sort()

举例:

// 1 .map()
// 1.1 使用 .forEach
var officers = [
    { id: 20, name: "Captiain Piett" },
    { id: 24, name: "General Veers" },
    { id: 56, name: "Admiral Ozzel" },
    { id: 88, name: "Commander Jerjerrod" },
];
var officersIds = [];

officers.forEach(item => officersIds.push(item.id))
console.log(JSON.stringify(officers)); // [{"id":20,"name":"Captiain Piett"},{"id":24,"name":"General Veers"},{"id":56,"name":"Admiral Ozzel"},{"id":88,"name":"Commander Jerjerrod"}]
console.log(JSON.stringify(officersIds)); // [20,24,56,88]

// 1.2 使用 .map
officers.map(item => item.id); // [20, 24, 56, 88]
// 等价于:
officers.map(function (item) {
    return item.id;
})
console.log(JSON.stringify(officers.map(item => item.id))); // [20, 24, 56, 88]

[1, 2, 4, 6, 3].map(item => item * 2); // [2, 4, 8, 12, 6]
// 等价于 jQuery 方法:
$.map([1, 2, 4, 6, 3], item => item * 2); // [2, 4, 8, 12, 6]

// 2 .reduce()
var pilots = [
    { id: 20, name: "Captiain Piett", years: 14 },
    { id: 24, name: "General Veers", years: 30 },
    { id: 56, name: "Admiral Ozzel", years: 16 },
    { id: 88, name: "Commander Jerjerrod", years: 22 },
];

pilots.reduce((initYear, item) => initYear + item.years, 0); // 82
pilots.reduce((oldest, item) => (oldest.years || 0) > item.years ? oldest : item, {}); // {id: 24, name: "General Veers", years: 30}

[1, 2, 4, 6, 3].reduce((initItem, item) => initItem + item * 2, 0); // 32

// 3 .filter()
var pilots = [
    { id: 20, name: "Captiain Piett", years: 14 },
    { id: 24, name: "General Veers", years: 30 },
    { id: 56, name: "Admiral Ozzel", years: 16 },
    { id: 88, name: "Commander Jerjerrod", years: 22 },
];
pilots.filter(item => item.name == "Commander Jerjerrod"); // [{id: 88, name: "Commander Jerjerrod", years: 22}]
console.log(JSON.stringify(pilots.filter(item => item.name == "Commander Jerjerrod")));

[1, 2, 4, 6, 3].filter(item => item >= 3); // [4, 6, 3]
// 等价于 jQuery 方法:
$.grep([1, 2, 4, 6, 3], item => item >= 3); // [4, 6, 3]

// 4 .map() 和 .filter() 相结合
[1, 2, 4, 6, 3].map(item => item * 2).filter(item => item >= 8); // [8, 12]

// 5 .sort()
[1, 2, 4, 6, 3].sort(); // [1, 2, 3, 4, 6]

3)Set和Map结构

举例:

// Set举例
var s1 = new Set([]);
s1.add(3);
s1.add(5);
s1.add(7);
s1.add(2);
s1.add(5);
for (let item of s1) {
    console.log(item); // 3 5 7 2
}
// 等价于
// s1.add(1).add(2).add(3).add(2)

// 数组去重
[...new Set([1, 2, 3, 4, 2, 3])]; // 1 2 3 4

// Map举例
var s1 = new Map([]);
s1.set('username', 'Hzhao');
s1.set('password', 'xxx');

// 使用数组结构直接获取key和value
for (let [key, value] of s1) {
    console.log(key + ": "); // username, password
    console.log(value); // Hzhao, xxx
}

4)举例

(1)数组/JSON数组去重

// 检查数组中元素是否存在重复
function checkElementIsRepeatInArray(arr, element) {
    var count = 0;
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] == element) {
            count++;
        }
    }

    return count >= 2 ? true : false;
}

// 检查元素是否在数组中
function checkElementIsInArray(element, array) {
    var check_result = false;

    for (var i = 0; i < array.length; i++) {
        if (element == array[i]) {
            check_result = true;
        }
    }
    return check_result;
}

// Example: var permission_country_code_arr = ObjArrTransferToArray(rsp.results, "country_code");
function ObjArrTransferToArray(object_array, property) {
    var res = [];
    if (!isNotNull(object_array) || !isNotNull(property)) {
        return res;
    }
    for (var i = 0; i < object_array.length; i++) {
        var value = object_array[i];
        if (isNotNull(value[property])) {
            res.push(value[property]);
        }
    }
    return res;
}

// var arr = [{'id': '1', 'item': 'Hello 1'}, {'id': '2', 'item': 'Hello 2'}, {'id': '3', 'item': 'Hello 1'}];
// arrayObjectDeduplication(arr, "item") // => [{'id': '1', 'item': 'Hello 1'}, {'id': '2', 'item': 'Hello 2'}]
/** 
数组对象去重 (会改变原数组),[{'id': '1', 'item': 'Hello 1'}, {'id': '2', 'item': 'Hello 2'}, {'id': '3', 'item': 'Hello 1'}]
@param {Array<*>} array
@param {String | Function} [by]
@return {Array<*>}
*/
function arrayObjectDeduplication(array, by) {
    var map = {};
    map.proto = null;
    for (var i = 0; i < array.length;) {
        var el = array[i];
        var id = by ? (typeof by == 'function' ? by(el, i) : el[by]) : el;
        if (map[id] === true) {
            array.splice(i, 1);
        } else {
            map[id] = true;
            i++;
        }
    }
    return array;
}

// 去除数组中的重复项
function arrayDeduplication(arr) {
    var newArr = [];
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] && newArr.indexOf(arr[i]) == -1) {
            newArr.push(arr[i]);
        }
    }
    return newArr;
}

// 举例: 输入 "11111;s00411111;s00411111", 要求 输出为: "11111;s00411111"
// var str1 = "11111;s00411111;s00411111";
// var str1_arr = "11111;s00411111;s00411111".split(";");
// var str1_new = arrayDeduplication(str1_arr).join(";"); // "11111;s00411111"
function arrayDeduplication(array) {
    array = array.sort();
    var result = array.length > 0 ? [array[0]] : [];

    for (var i = 1, len = array.length; i < len; i++) {
        array[i] !== array[i - 1] && result.push(array[i]);
    }
    return result;
}

3.2 Function 的属性和方法

语法总结:

// 1、Function 属性和方法
// 全局的Function对象没有自己的属性和方法, 但是, 因为它本身也是函数,所以它也会通过原型链从Function.prototype上继承部分属性和方法。
// 1.1 原型对象:属性
// (1)Function.arguments(此属性已被 arguments 替代)
// (2)Function.caller(获取调用函数的具体对象)
//      如果一个函数f是在全局作用域内被调用的,则f.caller为null,相反,如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数.
// (3)Function.length(获取函数的接收参数个数)
// (4)Function.name(获取函数的名称)
// (5)Function.displayName(获取函数的display name)
// (6)Function.prototype.constructor(声明函数的原型构造方法,详细请参考 Object.constructor, Object.prototype.constructor)

// 1.2 原型对象:方法
// (1)Function.prototype.apply()
//      在一个对象的上下文中应用另一个对象的方法;参数能够以数组形式传入
// (2)Function.prototype.call()
//      在一个对象的上下文中应用另一个对象的方法;参数能够以列表形式传入。
// (3)Function.prototype.bind()
//      bind()方法会创建一个新函数,称为绑定函数.当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数.
// (4)Function.prototype.toSource() 
//      获取函数的实现源码的字符串。 覆盖了 Object.prototype.toSource 方法。
// (5)Function.prototype.toString()
//      获取函数的实现源码的字符串。覆盖了 Object.prototype.toString 方法。

举例:

// 1、Function 属性和方法
// 全局的Function对象没有自己的属性和方法, 但是, 因为它本身也是函数,所以它也会通过原型链从Function.prototype上继承部分属性和方法。
// 1.1 原型对象:属性
// (1)Function.arguments(此属性已被 arguments 替代)
// 在函数递归调用时()在某一刻同一个函数运行了多次,也就是有多套实参,那么 arguments 属性的值就是最近一次该函数调用时传入的参数
// 如果函数不在执行期间,那么该函数的 arguments 属性的值是 null
console.log("\n");
function f1(n) { g1(n - 1); }
function g1(n) {
    console.log("1.1 before: " + g1.arguments[0]);
    if (n > 0) { f1(n); }
    console.log("1.2 after: " + g1.arguments[0]);
}
f1(2);
console.log("1.3 函数退出后的 arguments 属性值: " + g1.arguments);
// 输出:
// before: 1
// before: 0
// after: 0
// after: 1
// 函数退出后的 arguments 属性值: null

// (2)Function.caller(获取调用函数的具体对象,返回调用制定函数的函数)
//      如果一个函数f是在全局作用域内被调用的,则f.caller为null,相反,如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数.
console.log("\n");
function stop() { console.log("2.1 Stop function."); }
function f(n) {
    stop();
    if (f.caller == null) {
        console.log("2.2 n: " + n + "该函数在全局作用域内被调用!");
    } else
        console.log("2.3 n: " + n + "调用我的是函数是" + f.caller);
}
f(2);
// 输出:
// 2.1 Stop function.
// 2.2 n: 2该函数在全局作用域内被调用!

// (3)Function.length(获取函数的接收参数个数)
console.log("\n");
function sum(a, b) {
    return a + b;
}
console.log("sum.length: " + sum.length); // sum.length: 2

// (4)Function.name(获取函数的名称)
console.log("sum.name: " + sum.name); // sum.name: sum

// (5)Function.displayName(获取函数的显示名称)
console.log("sum.displayName: " + sum.displayName); // sum.displayName: undefined
// 当一个函数的 displayName 属性被定义,这个函数的 displayName 属性将返回显示名称
sum.displayName = "Sum Function";
console.log("sum.displayName: " + sum.displayName); // sum.displayName: Sum Function

// (6)Function.prototype.constructor(声明函数的原型构造方法,详细请参考 Object.constructor, Object.prototype.constructor)
// 所有的对象都会从它的原型上继承一个 constructor 属性
console.log("\n");
var o = {};
console.log("o.constructor === Object: " + (o.constructor === Object)); // true
var o = new Object;
console.log("o.constructor === Object: " + (o.constructor === Object)); // true
var a = [];
console.log("a.constructor === Array: " + (a.constructor === Array)); // true
var a = new Array;
console.log("a.constructor === Array: " + (a.constructor === Array)); // true
var n = new Number;
console.log("n.constructor === Number: " + (n.constructor === Number)); // true
// 创建一个原型:Tree,以及该类型的对象,即 theTree。然后打印 theTree对象的 constructor 属性
function Tree(name) {
    this.name = name;
}
var theTree = new Tree("Redwood");
console.log("theTree.constructor is " + theTree.constructor);

// 1.2 原型对象:方法
// (1)Function.prototype.apply()
//      语法:func.apply(thisArg, [argsArray])
//      thisArg:可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
//      argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或  undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
//      返回值:调用有指定this值和参数的函数的结果。
//      在一个对象的上下文中应用另一个对象的方法;参数能够以数组形式传入
// 注意:call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。
console.log("\n");
var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers); // 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..)
var min = Math.min.apply(null, numbers); // 基本等同于 Math.min(numbers[0], ...) 或 Math.min(5, 6, ..)
console.log("max: " + max + ", min: " + min);
// 用 apply() 将数组添加到另一个数组中
var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements); // max: 7, min: 2
console.log("array: " + array); // array: a,b,0,1,2

// (2)Function.prototype.call()
//      语法:fun.call(thisArg, arg1, arg2, ...)
//      thisArg:在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于non-strict mode,
//          则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。
//      arg1, arg2, ...:指定的参数列表。
//      返回值:使用调用者提供的this值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined。
//      在一个对象的上下文中应用另一个对象的方法;参数能够以列表形式传入。
// 1)使用call方法调用父构造函数
function Product(name, price) {
    this.name = name;
    this.price = price;
}
function Food(name, price) {
    Product.call(this, name, price);
    this.category = 'food';
}
function Toy(name, price) {
    Product.call(this, name, price);
    this.category = 'toy';
}
var cheese = new Food("feta", 5);
var fun = new Toy("robot", 40);
console.log("cheese: " + JSON.stringify(cheese)); // cheese: {"name":"feta","price":5,"category":"food"}
console.log("fun: " + JSON.stringify(fun)); // fun: {"name":"robot","price":40,"category":"toy"}
// 2)使用call方法调用匿名函数
var animals = [
    { species: 'Lion', name: 'King' },
    { species: 'Whale', name: 'Fail' }
];
for (var i = 0; i < animals.length; i++) {
    (function (i) {
        this.print = function () {
            console.log("#" + i + " " + this.species + ": " + this.name);
        }
        this.print();
    }).call(animals[i], i);
}
// 3)使用call方法调用函数并且没有确定第一个参数(argument)
var sData = 'Wisen';
function display() {
    console.log('sData value is %s ', this.sData);
}
display.call();  // sData value is Wisen, 在严格模式下this的值将会是undefined.

// (3)Function.prototype.bind()
//      bind()方法会创建一个新函数,称为绑定函数.
//      当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数.
// 1)创建绑定函数
var module = {
    x: 42,
    getX: function () {
        return this.x;
    }
};
console.log("module.getX(): " + module.getX());
var unboundGetX = module.getX;
console.log("unboundGetX: " + unboundGetX());
boundGetX = unboundGetX.bind(module); // 创建一个新函数,把 'this' 绑定到 module 对象
console.log("boundGetX: " + boundGetX());
// (4)Function.prototype.toSource() 
//      获取函数的实现源码的字符串。 覆盖了 Object.prototype.toSource 方法。
// (5)Function.prototype.toString()
//      获取函数的实现源码的字符串。覆盖了 Object.prototype.toString 方法。
console.log("module.toSource: " + display.toString());

3.3 Number

和其他编程语言(如 C 和 Java)不同,JavaScript 不区分整数值和浮点数值,所有数字在 JavaScript 中均用浮点数值表示,所以在进行数字运算的时候要特别注意。

console.log(0.1 + 0.2); // 0.30000000000000004

JavaScript 支持标准的算术运算符,包括加法、减法、取模(或取余)等等。还有一个之前没有提及的内置对象 Math(数学对象),用以处理更多的高级数学函数和常数:

// JavaScript 支持标准的算术运算符,包括加法、减法、取模(或取余)等等。还有一个之前没有提及的内置对象 Math(数学对象),用以处理更多的高级数学函数和常数
console.log(Math.sin(3.5)); // -0.35078322768961984
var r = 0.5;
var d = Math.PI * (r + r); // 3.141592653589793
console.log(d);

你可以使用内置函数 parseInt() 将字符串转换为整型。该函数的第二个参数表示字符串所表示数字的基(进制):

// 可以使用内置函数 parseInt() 将字符串转换为整型
// 该函数的第二个参数(可选)表示字符串所表示数字的基(进制)
console.log(parseInt("123")); // 123
console.log(parseInt("123", 10)); // 123
console.log(parseInt("010")); // 10
console.log(parseInt("010", 10)); // 10
console.log(parseInt("0x10")); // 16
console.log(parseInt("11", 2)); // 3

单元运算符+也可以把数字字符串转换成数值:

console.log(+ "42"); // 42
console.log(+ "010"); // 10
console.log(+ "0x10"); // 16

如果给定的字符串不存在数值形式,函数会返回一个特殊的值 NaN(Not a Number 的缩写),NaN作为参数进行过任何数学运算,结果也会是NaN。

console.log(parseInt("hello", 10)); // NaN
console.log(NaN + 5); // NaN

可以使用内置函数 isNaN() 来判断一个变量是否为 NaN:

console.log(isNaN(NaN)); // true

JavaScript 还有两个特殊值:Infinity(正无穷)和 -Infinity(负无穷):

可以使用内置函数 isFinite() 来判断一个变量是否是一个有穷数, 如果类型为Infinity, -Infinity 或 NaN则返回false:

console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity

// isFinit()判断数字是否是有限的
// 如果类型为Infinity, -Infinity 或 NaN则返回false
console.log(isFinite(1 / 0)); // false
console.log(isFinite(Infinity)); // false
console.log(isFinite(NaN)); // false
console.log(isFinite(-Infinity)); // false

console.log(isFinite(0)); // true
console.log(isFinite(2e64)); // true

console.log(isFinite('0')); // true,如果是纯数值类型的检测,则返回false:Number.isFinite("0");

JavaScript 还有一个类似的内置函数 parseFloat(),用以解析浮点数字符串,与parseInt()不同的地方是,parseFloat()只应用于解析十进制数字。

// parseFloat返回正常数字
console.log(parseFloat("3.14")); // 3.14
console.log(parseFloat("314e-2")); // 3.14
console.log(parseFloat("0.0314e2")); // 3.14
console.log(parseFloat("3.14more non-digit characters")); // 3.14
// 下面例子返回NaN
console.log(parseFloat("FF2")); // NaN

// 该函数通过正则表达式的方式,在需要更严格地转换float值时可能会有用
var filterFloat = function (value) {
    if(/^(\-|\+)?|(\.\d+)(\d+(\.\d+)?(\d+\.)|Infinity)$/.test(value)) {
        return Number(value);
    }
};
console.log(filterFloat('421')); // 421
console.log(filterFloat('-421')); // -421
console.log(filterFloat('+421')); // 421
console.log(filterFloat('Infinity')); // Infinity
console.log(filterFloat('1.61803398875')); // 1.61803398875
console.log(filterFloat('421e+0')); // 421
console.log(filterFloat('421hop')); // NaN
console.log(filterFloat('hop1.61803398875')); // NaN
console.log(filterFloat("999 888")); // NaN
console.log(filterFloat('.421')); // 0.421
console.log(filterFloat('421.')); // 421

parseInt() 和 parseFloat() 函数会尝试逐个解析字符串中的字符,直到遇上一个无法被解析成数字的字符,然后返回该字符前所有数字字符组成的数字。使用运算符 "+" 将字符串转换成数字,只要字符串中含有无法被解析成数字的字符,该字符串都将被转换成 NaN。请你用这两种方法分别解析“10.2abc”这一字符串,比较得到的结果,理解这两种方法的区别。

console.log(parseInt("10.2abc")); // 10
console.log(parseFloat("10.2abc")); // 10.2

举例:

// number.toExponential(fractionDigits)
// toExponential方法把这个number转换成一个指数形式的字符串
// 可选参数fractionDigits控制其小数点后的数字位数。它的值必须在0~2之间
console.log("Math.PI.toExponential(0): " + Math.PI.toExponential(0)); // Math.PI.toExponential(0): 3e+0
console.log("Math.PI.toExponential(2): " + Math.PI.toExponential(2)); // Math.PI.toExponential(2): 3.14e+0
console.log("Math.PI.toExponential(7): " + Math.PI.toExponential(7)); // Math.PI.toExponential(7): 3.1415927e+0
console.log("Math.PI.toExponential(16): " + Math.PI.toExponential(16)); // Math.PI.toExponential(16): 3.1415926535897931e+0
console.log("Math.PI.toExponential(): " + Math.PI.toExponential()); // Math.PI.toExponential(): 3.141592653589793e+0
console.log("----------------1 number.toExponential(fractionDigits) end----------------"); // ----------------1 number.toExponential(fractionDigits) end----------------

// number.toFixed(fractionDigits)
// toFixed方法把这个number转换成一个十进制数形式的字符串
// 可选参数fractionDigits控制其小数点后的数字位数。它的值必须在0~20之间,默认值为0
console.log("Math.PI.toFixed(0): " + Math.PI.toFixed(0)); // Math.PI.toFixed(0): 3
console.log("Math.PI.toFixed(2): " + Math.PI.toFixed(2)); // Math.PI.toFixed(2): 3.14
console.log("Math.PI.toFixed(7): " + Math.PI.toFixed(7)); // Math.PI.toFixed(7): 3.1415927
console.log("Math.PI.toFixed(16): " + Math.PI.toFixed(16)); // Math.PI.toFixed(16): 3.1415926535897931
console.log("Math.PI.toFixed(): " + Math.PI.toFixed()); // Math.PI.toFixed(): 3
console.log("----------------2 number.toFixed(fractionDigits) end----------------"); // ----------------2 number.toFixed(fractionDigits) end----------------

// number.toPrecision(precision)
// toFixed方法把这个number转换成一个十进制数形式的字符串
// 可选参数precision控制有效数字的位数。它的值必须在1~21之间。
console.log("Math.PI.toPrecision(1): " + Math.PI.toPrecision(1)); // Math.PI.toPrecision(1): 3
console.log("Math.PI.toPrecision(2): " + Math.PI.toPrecision(2)); // Math.PI.toPrecision(2): 3.1
console.log("Math.PI.toPrecision(7): " + Math.PI.toPrecision(7)); // Math.PI.toPrecision(7): 3.141593
console.log("Math.PI.toPrecision(16): " + Math.PI.toPrecision(16)); // Math.PI.toPrecision(16): 3.141592653589793
console.log("Math.PI.toPrecision(): " + Math.PI.toPrecision()); // Math.PI.toPrecision(): 3.141592653589793
console.log("----------------3 number.toPrecision(precision) end----------------"); // ----------------3 number.toPrecision(precision) end----------------

// number.toString(radix)
// toString方法把这个number转换成字符串
// 可选参数radix控制基数。它的值必须在2~36之间。默认的radix是以10为基数的。
// radix最常用的是整数,但是它可以用任意的数字
// 在最普通情况下,number.toString()可以更简单的写成String(number)
console.log("Math.PI.toString(2): " + Math.PI.toString(2)); // Math.PI.toString(2): 11.001001000011111101101010100010001000010110100011
console.log("Math.PI.toString(8): " + Math.PI.toString(8)); // Math.PI.toString(8): 3.1103755242102643
console.log("Math.PI.toString(16): " + Math.PI.toString(16)); // Math.PI.toString(16): 3.243f6a8885a3
console.log("Math.PI.toString(): " + Math.PI.toString()); // Math.PI.toString(): 3.141592653589793
console.log("String(Math.PI): " + String(Math.PI)); // String(Math.PI): 3.141592653589793
console.log("----------------4 number.toString(radix) end----------------"); // ----------------4 number.toString(radix) end----------------

3.4 Object

1)基本方法

// object.hasOwnProperty(name)
// 判断object是否包含name属性参数
var a = { member: "Hello, world" };
// var b = Object.beget(a);
var t = a.hasOwnProperty('member'); // t: true
console.log("t: " + t);
console.log("a.member: " + a.member); // a.member: Hello, world

// obj中属性为变量
var c = "hello";

var obj = {
    a: 12,
    b: 13,
    [c]: 14
};
obj.hello; // 14
c = "hah";
obj.hah; // undefined, 只有初始定义有效

// get set 方法的使用
var teacher = {
    firstname: '赵',
    lastname: '一纯',
    school: "sxt",
    learnFunc: function () {
        console.log("老师教学");
    },
    get fullname () {
        return this.firstname + '-' + this.lastname;
    },
    set fullname(data) {
        let arr = data.split('-');

        if(arr.length >= 2) {
            this.firstname = arr[0];
            this.lastname = arr[1];
        } else {
            console.error("输入的名字不规范");
        }
    }
}

2)Object对象的扩展

// student的原型是:Object
var student = {
    school: "sxt",
    learnFunc: function () {
        console.log("学生学习");
    }
};

// monitor的原型是:student
// 这样就实现了student的继承
// monitor除了拥有自身的属性之外,还拥有student的属性
var monitor = Object.create(student, {
    type: {
        value: "班长",
        writable: true, // default: false
        configurable: true, // default: false
        enumerable: true, // default: false
    },
    age: {
        value: "19",
        writable: false, // default: false
        configurable: true, // default: false
        enumerable: true, // default: false
    }
})
console.log(monitor.school); // sxt

console.log(monitor.type); // 班长
monitor.type = "副班长";
console.log(monitor.type); // 副班长

console.log(monitor.age); // 19
monitor.age = 22;
console.log(monitor.age); // 未生效,19

// 单次修改配置单个属性的修改
Object.defineProperty(monitor, "type", {
    writable: false
});
console.log(monitor.type); // 副班长
monitor.type = "排长";
console.log(monitor.type); // 副班长

// 多次修改配置
Object.defineProperties(monitor, {
    getMonitorInfo: {
        get: function () {
            console.log("Monitor information: type: " + this.type + ", age: " + this.age);
            return ("Monitor information: type: " + this.type + ", age: " + this.age);
        }
    }
});
monitor.getMonitorInfo(); // Monitor information: type: 副班长, age: 19

3.5 String

  1. string.charAt(pos ):返回在string中pos位置处的字符。

  2. string.charCodeAt(pos ):返回在string中pos位置处的字符的字符码位(整数形式表示)。

  3. string.concat(item...):很少被使用,因为'+'使用会更方便。

  4. string.indexOf(searchString, position):返回某个指定的字符串值在字符串中首次出现的位置。

  5. string.lastIndexOf(searchString, position):返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置从后向前搜索。

  6. string.localCompare(stringOther):localCompare方法比较两个字符串。如何比较字符串的规则没有详细的说明(从第一个字符开始比较),如果string比字符串that小,那么结果为负数;如果它们是相等的,那么结果为0。

  7. string.match(regexp):在字符串内检索指定的值,返回值

    (1)JS匹配字符串中两个子字符串之间的字符串

    var str = "aaa好的bd123内容kkk内容k好的2内容";
    str = str.match(/好的(\S*)内容/)[1];
    
  8. string.replace(searchValue, replaceVale):str.replace(/Microsoft/g, "W3School") 全局替换

  9. string.search(regexp):stringObject 中第一个与 regexp 相匹配的子串的起始位置

  10. string.slice(start, end):slice方法复制string的一部分来构造一个新的字符串。

  11. string.substring(start, end):提取字符串中介于两个指定下标之间的字符。

  12. string.toLocaleLowerCase():使用本地化的规则把这个string中的所有字母转换为小写格式。这个方法主要用在土耳其语上,因为在土耳其语中'I'转换为'l',而不是'i'

  13. string.fromCharCode(char...):该函数从一串数字中返回一个字符串,var a1 = String.fromCharCode(67, 97, 116); // Cat

  14. join:把数组中的所有元素放入一个字符串。

  15. split:把一个字符串分割成字符串数组。

  16. toString():把一个逻辑值转换为字符串,并返回结果。

  17. valueOf():返回 Boolean 对象的原始值。

  18. JSON.parse('text'):字符串转对象

  19. JSON.stringify(value):对象转为字符串

举例:

// string.charAt(pos )
// charAt方法返回在string中pos位置处的字符
// 如果pos小于0或者大于等于字符串的长度string.length,它会返回空字符串
var name = "Curly";
var initial = name.charAt(0); // C
console.log("initial: " + initial);
console.log("----------------1 string.charAt(pos ) end----------------");

// string.charCodeAt(pos )
// charCodeAt方法返回在string中pos位置处的字符的字符码位(整数形式表示)
// 如果pos小于0或者大于等于字符串的长度string.length,它会返回NaN
var name = "Curly";
var initial = name.charCodeAt(0); // 67
console.log("initial: " + initial);
console.log("----------------2 string.charCodeAt(pos ) end----------------");

// string.concat(item...),很少被使用,因为'+'使用会更方便
var str = 'C'.concat('a', 't'); // Cat
console.log("str: " + str);
console.log("----------------3 string.concat(item...) end----------------");

// string.indexOf(searchString, position)
// indexOf方法在string中查找另一个字符串searchString。
// 如果它被找到,则返回第一个匹配字符的位置,否则返回-1。
// 可选参数position可设置从string的某个指定的位置开始查找
var text = "Mississippi";
var p1 = text.indexOf('ss');
var p2 = text.indexOf('ss', 3);
var p3 = text.indexOf('ss', 6);
console.log("p1: " + p1 + ", p2: " + p2 + ", p3: " + p3); // p1: 2, p2: 5, p3: -1
console.log("----------------4 string.indexOf(searchString, position) end----------------");

// string.lastIndexOf(searchString, position)
// lastIndexOf方法在string中(从末尾开始)查找另一个字符串searchString。
// 如果它被找到,则返回第一个匹配字符的位置(即从后往前找的第一个匹配字符的位置),否则返回-1。
// 可选参数position可设置从string的某个指定的位置开始查找
var text = "Mississippi";
var p1 = text.lastIndexOf('ss');
var p2 = text.lastIndexOf('ss', 3);
var p3 = text.lastIndexOf('ss', 6);
console.log("p1: " + p1 + ", p2: " + p2 + ", p3: " + p3); // p1: 5, p2: 2, p3: 5
console.log("----------------5 string.lastIndexOf(searchString, position) end----------------");

// string.localCompare(stringOther)
// localCompare方法比较两个字符串。如何比较字符串的规则没有详细的说明(从第一个字符开始比较)
// 如果string比字符串that小,那么结果为负数;如果它们是相等的,那么结果为0
var str1 = "Mississippi";
var str2 = "Mississippi, hahaha";
var result1 = str1.localeCompare(str2); // -1
var result2 = str2.localeCompare(str1); // 1
console.log("6.1 result1: " + result1 + ", ressult2: " + result2);
var str1 = "Mississippi";
var str2 = "Mississippi";
var result1 = str1.localeCompare(str2); // 0
var result2 = str2.localeCompare(str1); // 0
console.log("6.2 result1: " + result1 + ", ressult2: " + result2);
var str1 = "aississippi";
var str2 = "bississipp, hahaha";
var result1 = str1.localeCompare(str2); // -1
var result2 = str2.localeCompare(str1); // 1
console.log("6.3 result1: " + result1 + ", ressult2: " + result2);
var str1 = "bississippi";
var str2 = "aississipp, hahaha";
var result1 = str1.localeCompare(str2); // 1
var result2 = str2.localeCompare(str1); // -1
console.log("6.4 result1: " + result1 + ", ressult2: " + result2);
console.log("----------------6 string.localCompare(stringOther) end----------------");

// string.match(regexp)
// match方法匹配一个字符串和一个正则表达式
// 它依据g标识来决定如何进行匹配。如果没有g标识,那么调用string.match(regexp)的结果与调用regexp.exec(string)的结果相同
// 如果带有g标识,那么它返回一个包含捕获分组之外的所有匹配的数组
var text = '<html><body bgcolor=linen><p>' + 'This is <b>bold<\/b>!<\/p><\/body><\/html>';
var tags = /[^<>]+|<(\/?)(a-zA-Z+)([^<>]*)>/g;
var a = text.match(tags);
console.log(a);
console.log("----------------7 string.match(regexp) end----------------");

// string.replace(searchValue, replaceVale)
// 查找和替换操作,并返回一个新的字符串
// searchValue可以是一个字符串或者一个正则表达式对象
// 如果searchValue它是一个字符串,那么searchValue只会在第一次出现的地方被替换
var result = "mother_in_law".replace('_', '-');
console.log("result: " + result); // result: mother-in_law
// 如果searchValue它是一个正则表达式并且带有g标志,那么searchValue会替换所有匹配支出,如果没有带g标识,那么它将仅替换第一个匹配之处。
// 捕获括号中的3个数字
var oldaracode = /\((d{3})\)/g;
var p = '(555)666-1212'.replace(oldaracode, '$1-');
console.log("p: " + p); // p: (555)666-1212
console.log("----------------8 string.replace(searchValue, replaceVale) end----------------");

// string.search(regexp)
// search方法和indexOf方法类似,只是它只接受一个正则表达式对象作为参数而不是一个字符串。
// 如果找到匹配,它返回第一个匹配的首字符位置,如果没有找到匹配,则返回-1,此方法会忽略g标志,且没有position参数
var text = 'and in it he says "Any damn fool could"';
var pos1 = text.search(/["']/);
var pos2 = text.search(/[h]/);
console.log("pos1: " + pos1 + ", pos2: " + pos2);
console.log("----------------9 string.search(regexp) end----------------");

// string.slice(start, end)
// slice方法复制string的一部分来构造一个新的字符串。
// 如果start参数是负值,它将于string.length相加。即截取从最后往前数的多少个字符
// end参数是可选的,并且它的默认值是string.length,如果end参数是负数,那么它将于string.length相加。
var text = 'and in it he says "Any damn fool could"';
var a = text.slice(18);
var b = text.slice(0, 3);
var c = text.slice(-5);
var d = text.slice(18);
console.log("a: " + a);
console.log("b: " + b);
console.log("c: " + c);
console.log("d: " + d);
console.log("----------------10 string.slice(start, end) end----------------");

// string.substring(start, end)
// substring的用法与slice方法一样,只是substring方法不能处理负数参数。
// 没有任何理由去使用substring方法。请用slice替代它。

// string.split(separator, limit)
// split方法把这个string分割成片段来创建一个字符串数组。
// 可选参数limit可以限制被分割的片段数量。
// separator参数可以是一个字符串或者一个正则表达式
// 如果separator参数是一个空字符串,将返回一个单字符的数组
var str_digits = '0123456789';
var a = str_digits.split('', 5);
console.log(a); // [ '0', '1', '2', '3', '4' ]
var ip = "192.168.1.0";
var b = ip.split('.');
console.log(b); // [ '192', '168', '1', '0' ]
var c1 = '|a|b|c'.split('|');
console.log(c1); // [ '', 'a', 'b', 'c' ]
var c2 = '|a|b|c'.split('|');
console.log(c2); // [ '', 'a', 'b', 'c' ]
var text = 'last, first, middle';
var d1 = text.split(/\s*, \s*/);
console.log(d1); // [ 'last', 'first', 'middle' ]
var d1 = text.split(/\s*(,)\s*/);
console.log(d1); // [ 'last', ',', 'first', ',' 'middle' ]
var d2 = text.split(/\s*/);
console.log(d2); // 'l', 'a', ...,单个字符组成的数组
console.log("----------------11 string.split(start, end) end----------------");

// string.toLocaleLowerCase()
// 使用本地化的规则把这个string中的所有字母转换为小写格式。
// 这个方法主要用在土耳其语上,因为在土耳其语中'I'转换为'l',而不是'i'
var text = "Hello, world, PI, Pi";
var str1 = text.toLocaleLowerCase();
console.log("str1: " + str1); // hello, world, pi, pi
// string.toLocaleUpperCase()
// 使用本地化的规则把这个string中的所有字母转换为小写格式。这个方法主要用在土耳其语上
var str2 = text.toLocaleUpperCase();
console.log("str2: " + str2); // HELLO, WORLD, PI, PI
// string.toLowerCase(),返回一个新的字符串,这个string中的所有字母都被转化为小写格式
var str3 = text.toLowerCase();
console.log("str3: " + str3); // hello, world, pi, pi
// string.toUpperCase(),返回一个新的字符串,这个string中的所有字母都被转化为小写格式
var str4 = text.toUpperCase();
console.log("str4: " + str4); // HELLO, WORLD, PI, PI
console.log("----------------12 string.toLocaleLowerCase/toLocaleUpperCase/toLowerCase/toUpperCase end----------------");

// string.fromCharCode(char...),该函数从一串数字中返回一个字符串
var a1 = String.fromCharCode(67, 97, 116);
console.log("a1: " + a1); // a1: Cat
var nums = (67, 97, 116);
var a2 = String.fromCharCode(nums);
console.log("a2: " + a2); // a2: t
var nums = '67, 97, 116';
var a3 = String.fromCharCode(nums);
console.log("a3: " + a3); // a3: 
console.log("----------------13 string.fromCharCode(char...) end----------------");

1)判空函数和判非空函数

// JS默认判空场景: false, 0, NaN, "", undefined, null
// 举例:
if(undefined) {
    console.error("xxx");
}

// 判空函数
function isNull(value) {
    if (value == null || value == undefined) {
        return true;
    }
    var valstr = value.toString();
    if (valstr == 'null' || valstr == 'undefined' || valstr == '') {
        return true;
    }
    return false;
}


// 判非空函数
function isNotNull(value) {
    if (value == null || value == undefined) {
        return false;
    }
    var valstr = value.toString();
    if (valstr == 'null' || valstr == 'undefined' || valstr == '') {
        return false;
    }
    return true;
}

2)判断字符串中是否包含中文

escape对字符串进行编码时,字符值大于255的以"%u****"格式存储,而字符值大于255的恰好是非英文字符(一般是中文字符,非中文字符也可以当作中文字符考虑);indexOf用以判断在字符串中是否存在某子字符串,找不到返回"-1"。

<script language="javascript">  
    var str='中国';  
    if(escape(str).indexOf("%u")<0){   
        alert("没有包含中文");  
    }  
    else{  
        alert("包含中文");  
    }  
</script>

3.6 Boolean

3.7 Symbol(符号)(第六版新增)

3.8 Null(空)和 Undefined(未定义)

JavaScript 中 null 和 undefined 是不同的,前者表示一个空值(non-value),必须使用null关键字才能访问,后者是“undefined(未定义)”类型的对象,表示一个未初始化的值,也就是还没有被分配的值。我们之后再具体讨论变量,但有一点可以先简单说明一下,JavaScript 允许声明变量但不对其赋值,一个未被赋值的变量就是 undefined 类型。还有一点需要说明的是,undefined 实际上是一个不允许修改的常量。

JavaScript 包含布尔类型,这个类型的变量有两个可能的值,分别是 true 和 false(两者都是关键字)。根据具体需要,JavaScript 按照如下规则将变量转换成布尔类型:

  1. false, 0, 空字符串(""), NaN, null, undefined 被转换为false;
  2. 所有其它值被转换为true。
console.log(Boolean('')); // false
console.log(Boolean(234)); // true

不过一般没必要这么做,因为 JavaScript 会在需要一个布尔变量时隐式完成这个转换操作(比如在 if 条件语句中)。所以,有时我们可以把转换成布尔值后的变量分别称为 真值(true values)——即值为 true 和 假值(false values)——即值为 false;也可以分别称为“真的”(truthy)和“假的”(falsy)。

3.9 基本类型和引用类型及判断JS数据类型的四种方法

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:

  • 基本类型:String、Number、Boolean、Symbol、Undefined、Null
  • 引用类型:Object

基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。

引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。

鉴于 ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型。对于这个问题,JavaScript 也提供了多种方法,但遗憾的是,不同的方法得到的结果参差不齐。

下面介绍常用的4种方法,并对各个方法存在的问题进行简单的分析。

1)typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

有些时候,typeof 操作符会返回一些令人迷惑但技术上却正确的值:

  • 对于基本类型,除 null 以外,均可以返回正确的结果。
  • 对于引用类型,除 function 以外,一律返回 object 类型。
  • 对于 null ,返回 object 类型。
  • 对于 function 返回 function 类型。

其中,null 有属于自己的数据类型 Null , 引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。

2)instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。

理解原型和原型链:

var one = {x: 1};
var two = new Object();
one.__proto__ === Object.prototype // true
two.__proto__ === Object.prototype // true
one.toString === one.__proto__.toString // true

在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:

instanceof (A, B) = {
    var L = A.__proto__;
    var R = B.prototype;
    if (L === R) {
        // A的内部属性 __proto__ 指向 B 的原型对象
        return true;
    }
    return false;
}

从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:

[] instanceof Array; // true
{} instanceof Object; // true
new Date() instanceof Date; // true

function Person() {};
new Person() instanceof Person;

[] instanceof Object; // true
new Date() instanceof Object; // true
new Person instanceof Object; // true

我们发现,虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?

我们来分析一下 [ ]、Array、Object 三者之间的关系:

从 instanceof 能够判断出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最终 Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:

__proto__ __proto__ __proto__ null
[] Array.prototype Object.prototype

从原型链可以看出,[] 的 __proto__ 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr = new xArray(1, 2, 3); // [1,2,3]
arr instanceof Array; // false

针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。

if (Array.isArray(value)) {
    // 对数组执行某些操作
}

Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。

3)constructor

当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。

当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F。

可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。

4)toString

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call(''); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
Object.prototype.toString.call(document); // [object HTMLDocument]
Object.prototype.toString.call(window); //[object global] window 是全局对象 global 的引用

3.10 时间处理相关(获取当前Local/UTC时间)

// 1. 获取当前时间(标准时间格式)
// 1.1 获取当前时间:YYYY-MM-DD
function getCurrentTimeLocal() {
    var date = new Date();
    var currentdate = date.getFullYear() + '-' +
        ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getDate()).slice(-2);
    return currentdate;
}
getCurrentTimeLocal(); // YYYY-MM-DD

// 1.2.1 获取当前本地时间:YYYY-MM-DD hh:mm:ss
function getCurrentTimeLocal() {
    var date = new Date();
    var currentdate = date.getFullYear() + '-' +
        ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getDate()).slice(-2) + ' ' +
        ('0' + date.getHours()).slice(-2) + ':' +
        ('0' + date.getMinutes()).slice(-2) + ':' +
        ('0' + date.getSeconds()).slice(-2);
    return currentdate;
}
getCurrentTimeLocal(); // YYYY-MM-DD hh:mm:ss

// 1.2.2 获取当前UTC时间:YYYY-MM-DD hh:mm:ss
function getCurrentTimeUTC() {
    var date = new Date();

    var currentdate = date.getUTCFullYear() + '-' +
        ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getUTCDate()).slice(-2) + ' ' +
        ('0' + date.getUTCHours()).slice(-2) + ':' +
        ('0' + date.getUTCMinutes()).slice(-2) + ':' +
        ('0' + date.getUTCSeconds()).slice(-2);
    return currentdate;
}
getCurrentTimeUTC(); // YYYY-MM-DD hh:mm:ss

// 1.3.1 localToUTC:YYYY-MM-DD hh:mm:ss
function localToUTC(localTime) {
    var date = new Date(localTime);

    var currentdate = date.getUTCFullYear() + '-' +
        ('0' + (date.getUTCMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getUTCDate()).slice(-2) + ' ' +
        ('0' + date.getUTCHours()).slice(-2) + ':' +
        ('0' + date.getUTCMinutes()).slice(-2) + ':' +
        ('0' + date.getUTCSeconds()).slice(-2);
    return currentdate;
}
localToUTC("2020-01-01 18:00:00"); // "2020-01-01 10:00:00"

// 1.4.1 转换成任意格式的时间格式
function formatDate(date, fmt) {
    var s = "", n = 0, t = "";
    var formatJson = {
        "Y": date.getYear() + 1900,
        "M": date.getMonth() + 1,
        "D": date.getDate(),
        "h": date.getHours(),
        "m": date.getMinutes(),
        "s": date.getSeconds(),
        "Q": Math.floor((date.getMonth() + 3) / 3) // 季度
    }
    for (var k in formatJson) {
        s = fmt.match(new RegExp(k, "g"));
        if (s && ("Y" == k || "M" == k || "D" == k)) {
            n = s.length;
            t = formatJson[k] >= 10 ? formatJson[k] + "" : "0" + formatJson[k] + "";
            fmt = fmt.replace(new RegExp(k + "+", "g"), t.slice(-n));
        } else if (s) {
            n = s.length;
            t = formatJson[k] >= 10 ? formatJson[k] + "" : "0" + formatJson[k] + "";
            fmt = fmt.replace(new RegExp(k + "+", "g"), t);
        }
    }
    return fmt;
}
formatDate(new Date(), "YYYY-MM-DD hh:mm:ss"); // 2020-04-07 21:00:09


// 1.5 获取当前Local/UTC时间(基于下面的 Date 添加的属性)
function getCurrentTimeLocal() {
    return new Date(new Date().localtoUTC()).formatDate("YYYY-MM-DD hh:mm:ss");
}

function getCurrentTimeUTC() {
    return new Date(new Date()).formatDate("YYYY-MM-DD hh:mm:ss");
}

// 1.5.1 转换成任意格式的时间格式
Date.prototype.formatDate = function (fmt) {
    var s = "", n = 0, t = "";
    var formatJson = {
        "Y": this.getYear() + 1900,
        "M": this.getMonth() + 1,
        "D": this.getDate(),
        "h": this.getHours(),
        "m": this.getMinutes(),
        "s": this.getSeconds(),
        "Q": Math.floor((this.getMonth() + 3) / 3) // 季度
    }
    for (var k in formatJson) {
        s = fmt.match(new RegExp(k, "g"));
        if (s && ("Y" == k || "M" == k || "D" == k)) {
            n = s.length;
            t = formatJson[k] >= 10 ? formatJson[k] + "" : "0" + formatJson[k] + "";
            fmt = fmt.replace(new RegExp(k + "+", "g"), t.slice(-n));
        } else if (s) {
            n = s.length;
            t = formatJson[k] >= 10 ? formatJson[k] + "" : "0" + formatJson[k] + "";
            fmt = fmt.replace(new RegExp(k + "+", "g"), t);
        }
    }
    return fmt;
}

// 本地时间转UTC时间,结果返回一个时间戳
Date.prototype.localtoUTC = function () {
    return this.getTime() + this.getTimezoneOffset() * 60000;
}

// UTC时间转本地时间,结果返回一个时间戳
Date.prototype.utctoLocal = function () {
    return this.getTime() - this.getTimezoneOffset() * 60000;
}

// 获取举例今天的第几天的日期,结果返回一个时间戳
Date.prototype.getNdate = function (num) {
    if (arguments.length === 0) num = 0;
    var time = this.getTime();
    num = 1000 * 60 * 60 * 24 * num;
    time = time + num;
    return time;
}

// 2. 将时间戳转换成标准的时间格式,1398250549123: 2014-04-23 18:55:49
// 2.1 将时间戳转换成:YYYY-MM-DD
function getTimeStamp1(timeStamp) {
    var date = new Date(timeStamp);
    var currentdate = date.getFullYear() + '-' +
        ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getDate()).slice(-2);
    return currentdate;
}
getTimeStamp1(1398250549123); // 2014-04-23

// 2.2 将时间戳转换成:YYYY-MM-DD hh:mm:ss
function getTimeStamp2(timeStamp) {
    var date = new Date(timeStamp);
    var currentdate = date.getFullYear() + '-' +
        ('0' + (date.getMonth() + 1)).slice(-2) + '-' +
        ('0' + date.getDate()).slice(-2) + ' ' +
        ('0' + date.getHours()).slice(-2) + ':' +
        ('0' + date.getMinutes()).slice(-2) + ':' +
        ('0' + date.getSeconds()).slice(-2);
    return currentdate;
}
getTimeStamp2(1398250549123); // 2014-04-23 18:55:49


// 3. 将标准时间格式转换为时间戳
var startTime = '2014-04-23 18:55:49:123';
startTime = startTime.replace(/-/g, '/'); // 可选操作:"2014-04-23 18:55:49:123" 转换为 "2014/04/23 18:55:49:123"
console.log("startTime: " + startTime);
var date = new Date(startTime);
console.log("date: " + date); // Wed Apr 23 2014 18:55:49 GMT+0800 (中国标准时间)

var time1 = date.getTime(); // 精确到毫秒
var time2 = date.valueOf(); // 精确到毫秒
var time3 = Date.parse(date); // 精确到秒,毫秒将用 0 来代替
console.log("time1: " + time1); // 1398250549123
console.log("time2: " + time2); // 1398250549123
console.log("time3: " + time3); // 1398250549000

3.12 JavaScript Promise

1)Promise介绍

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。

ES6中的promise对象,可以将操作以同步的流程表达出来,很好地解决了回调地狱的问题(如:ajax中层层嵌套的回调函数),在使用ES5的时候,在多层嵌套回调时,写的代码层次过多,很难进行维护和二次开发。

语法:

new Promise(function (resolve, reject) { /* executor */ });

参数(executor):

executor是带有 resolve 和 reject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回所建promise实例对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用resolve函数来将promise状态改成fulfilled,要么调用reject 函数将promise的状态改为rejected。如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

一个 Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

pending 状态的 Promise 对象可能会变为fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用。

不要和惰性求值混淆: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如Scheme. Javascript中的promise代表一种已经发生的状态, 而且可以通过回调方法链在一起。 如果你想要的是表达式的延时计算,考虑无参数的"箭头方法": f = () =>``表达式 创建惰性求值的表达式,使用 f() 求值。

注意: 如果一个promise对象处在fulfilled或rejected状态而不是pending状态,那么它也可以被称为settled状态。你可能也会听到一个术语resolved ,它表示promise对象处于fulfilled状态。关于promise的术语, Domenic Denicola 的 States and fates 有更多详情可供参考。

举例:

// 举例1
var promise1 = new Promise(function (resolve, reject) {
    if (true) { // 执行成功的情况
        resolve("Resolve test");
    }
    if (false) {
        reject("Reject test");
    }
});
promise1.then(function (value) {
    console.log("1 OKay, value: " + value);
}).catch(function (e) {
    console.log("2 Failed, error: " + e);
});
// 等价于,方便理解,命名函数名称
promise1.then(function onFulfilled(value) {
    console.log("1 OKay, value: " + value);
}).catch(function onRejected(e) {
    console.log("2 Failed, error: " + e);
});

// 等价于 promise.then(onFulfilled, onRejected)
promise1.then(function (value) {
    console.log("1 OKay, value: " + value);
}, function (e) {
    console.log("2 Failed, error: " + e);
});


// 举例2
var promise2 = new Promise(function (resolve, reject) {
    if (false) {
        resolve("Resolve test");
    }
    if (true) { // 执行失败的情况
        reject("Reject test");
    }
});
promise2.then(function (value) {
    console.log("1 OKay, value: " + value);
}).catch(function (e) {
    console.log("2 Failed, error: " + e);
});
// 等价于,方便理解,命名函数名称
promise2.then(function onFulfilled(value) {
    console.log("1 OKay, value: " + value);
}).catch(function onRejected(e) {
    console.log("2 Failed, error: " + e);
});
// 等价于 promise.then(onFulfilled, onRejected)
promise2.then(function (value) {
    console.log("1 OKay, value: " + value);
}, function (e) {
    console.log("2 Failed, error: " + e);
});

2)Promise方法

Promise.all(iterable)

这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。

Promise.race(iterable)

当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
Promise.reject(reason)

返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法。
Promise.resolve(value) // 作为 new Promise() 的快捷方式,在进行promise对象的初始化或者编写测试代码的时候都非常方便

返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。

  • then() 方法返回一个 Promise 。它最多需要有两个参数:Promise 的成功和失败情况的回调函数。
  • catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。 (事实上, calling obj.catch(onRejected) 内部calls obj.then(undefined, onRejected)).
  • finally() 方法返回一个Promise,在promise执行结束时,无论结果是fulfilled或者是rejected,在执行then()和catch()后,都会执行finally指定的回调函数。这为指定执行完promise后,无论结果是fulfilled还是rejected都需要执行的代码提供了一种方式,避免同样的语句需要在then()和catch()中各写一次的情况。

举例1:

// 1. Promise.resolve
Promise.resolve(3);
// 等价于以下代码的语法糖
new Promise(function (resolve) {
    return resolve(3);
});
// 等价于
new Promise(function (resolve, reject) {
    return resolve(3);
});
Promise.resolve(3).then(function (val) {
    console.log(val) // 3
});

var promise1 = Promise.resolve("Hello, world!");
promise1.then((param) => console.log(param)); // Hello, world!

// 2.1 Promise.reject
Promise.reject("Error"); // Uncaught (in promise) Error
// 等价于以下代码的语法糖
new Promise(function (reject) { // Uncaught (in promise) Error
    return reject("Error");
});
// 等价于
new Promise(function (resolve, reject) {
    return reject("Error");
});
Promise.reject("Error").catch(function (e) {
    console.log(e) // "Error"
});

// 2.2 Promise.reject
var promise = Promise.reject(new Error("Error message"));
promise.catch(function (e) {
    console.log(e);
});

var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve("foo"), 100);
})

// Promise.all 等待所有都完成(或第一个失败)
Promise.all([promise1, promise2, promise3]).then(function (values) {
    console.log(values);
});

举例2:

// 举例1
var resolvedPromisesArray1 = [Promise.resolve(33), Promise.resolve(44)];
var p1 = Promise.all(resolvedPromisesArray1).then(function (values) { console.log("1-1: " + values) }).catch(function (reasons) { console.log("1-2: " + reasons) });
// immediately logging the value of p
console.log(p1);

// 举例2
var resolvedPromisesArray2 = [Promise.resolve(33), Promise.reject(44)];
var p2 = Promise.all(resolvedPromisesArray2).then(function (values) { console.log("2-1: " + values) }).catch(function (reasons) { console.log("2-2: " + reasons) });
// immediately logging the value of p
console.log(p2);

// 举例3
var resolvedPromisesArray3 = [Promise.reject(33), Promise.resolve(44)];
var p3 = Promise.all(resolvedPromisesArray3).then(function (values) { console.log("3-1: " + values) }).catch(function (reasons) { console.log("3-2: " + reasons) });
// immediately logging the value of p
console.log(p3);

举例3:

// 举例1
var resolvedPromisesArray1 = [Promise.resolve(33), Promise.resolve(44)];
var p1 = Promise.race(resolvedPromisesArray1).then(function (values) { console.log("1-1: " + values) }).catch(function (reasons) { console.log("1-2: " + reasons) });
// immediately logging the value of p
console.log(p1);

// 举例2
var resolvedPromisesArray2 = [Promise.resolve(33), Promise.reject(44)];
var p2 = Promise.race(resolvedPromisesArray2).then(function (values) { console.log("2-1: " + values) }).catch(function (reasons) { console.log("2-2: " + reasons) });
// immediately logging the value of p
console.log(p2);

// 举例3
var resolvedPromisesArray3 = [Promise.reject(33), Promise.resolve(44)];
var p3 = Promise.race(resolvedPromisesArray3).then(function (values) { console.log("3-1: " + values) }).catch(function (reasons) { console.log("3-2: " + reasons) });
// immediately logging the value of p
console.log(p3);

举例4(Promise.all 和 Promise.race 的差异):

function timerPromisefy(delay) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    })
}

// 1. Promise.all
var startDate = Date.now();
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128),
]).then(function (values) {
    console.log(Date.now() - startDate + "ms"); // => 139ms
    console.log(values); // [1, 32, 64, 128]
});

// 2. Promise.race
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128),
]).then(function (values) {
    console.log(values); // 1
});

举例5:

var p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 100, "two");
});

Promise.race([p1, p2]).then(function (value) {
    console.log(value); // "two"
    // 两个都完成,但 p2 更快
});

var p3 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 100, "three");
});
var p4 = new Promise(function (resolve, reject) {
    setTimeout(reject, 500, "four");
});

Promise.race([p3, p4]).then(function (value) {
    console.log(value); // "three"
    // p3 更快,所以它完成了
}, function (reason) { // 等价于 .catch(function () { /* ... */ })
    // 未被调用
});

var p5 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 500, "five");
});
var p6 = new Promise(function (resolve, reject) {
    setTimeout(reject, 100, "six");
});

Promise.race([p5, p6]).then(function (value) {
    // 未被调用
}, function (reason) { // 等价于 .catch(function () { /* ... */ })
    console.log(reason); // "six"
    // p6 更快,所以它失败了
});

3)Promise实现延时

// 从JS运行机制分析(异步执行),实际时间会大于等于 ms 毫秒
// 方式一
var delayType1 = function (ms) {
    return new Promise(function (resolve) { setTimeout(resolve, ms) })
};
// 方式二
var delayType2 = function (ms) {
    return new Promise((resolve) => setTimeout(resolve, ms))
};
// 方式三
var delayType3 = ms => new Promise(resolve => setTimeout(resolve, ms));

// 使用
delayType1(1000).then(function () {
    console.log("delayType1 Hello");
})
delayType2(1000).then(function () {
    console.log("delayType2 Hello");
})
delayType3(1000).then(function () {
    console.log("delayType3 Hello");
})

4)Promise Chain (promise-then-catch-flow)

举例1:

function taskA() {
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);


// 输出结果
// Task A
// Task B
// Final Task

举例2(taskA 产生异常):

function taskA() {
    console.log("Task A");
    throw new Error("throw Error @ Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);


// 输出结果:
// Task A
// Error: throw Error @ Task A
// Final Task 

5)Promise Chain Passing Paramters (promise-then-passing-value)

function doubleUp(value) {
    return value * 2;
}
function increment(value) {
    return value + 1;
}
function output(value) {
    console.log(value);// => (1 + 1) * 2
}

var promise = Promise.resolve(1);
promise
    .then(increment)
    .then(doubleUp)
    .then(output)
    .catch(function(error){
        // promise chain中出现异常的时候会被调用
        console.error(error);
    });


// 输出结果:4

6)每次调用then都返回一个新创建的promise对象

var aPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = aPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
    resolve(100);
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    console.log("1: " + value); // => 100
})

// vs

// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
    resolve(100);
});
bPromise.then(function (value) {
    return value * 2;
}).then(function (value) {
    return value * 2;
}).then(function (value) {
    console.log("2: " + value); // => 100 * 2 * 2
});



// then 返回新创建的 promise 对象
function anAsynCall() {
    var promise = Promise.resolve();
    return promise.then(function () {
        // To Do
        return "Haha";
    });
}

7)Promise.then(onFulfilled, onRejected)、Promise.then(onFulfilled).catch(onRejected)差异性

  1. Promise.then(onFulfilled, onRejected),在onFulfilled中发生异常的话,在onRejected中是捕获不到这个异常的;
  2. Promisie.then(onFulfilled).catch(onRejected),在onFulfilled中发生异常的话,在onRejected中是捕获不到这个异常的;
  3. .then和.catch本质上是没有区别的,需要分场合使用。

3.13 async/await

async/await 可以很好的解决了多层回调嵌套的问题,使得异步代码编写的更加简洁。

async 会将其后的函数封装成一个Promise对象,await会等待这个Promise完成,将其resolve的结果返回回来。

async 使用

async 放在函数声明之前,使其成为一个异步函数(promise)

async function hello() {
    return "Hello, world";
}

var hello = async () => { return "hello, world" };

hello().then(function (data) { console.log(data) }); // hello, world
await hello(); // hello, world

await 使用

function getSomethong() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("Hello, async.");
}

async function test() {
    const v1 = await getSomethong();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test() // something Hello, async

可以在 async 函数中,对 promise 的函数前面加上 await,await会拿到resolve结果,是 then函数的语法糖。

举例1:

var p1 = () => new Promise((res, rej) =>
    setTimeout(() => {
        res("Hello, world!");
    }, 1000)
);

async function q1() {
    var res = await p1();
    console.log(res);
}

q1(); // 1s之后输出:Hello, world!

// 等价于:
function q1() {
    p1().then(function (data) {
        console.log(data);
    });
}

q1(); // 1s之后输出:Hello, world!

举例2:

var p1 = () => new Promise((res, rej) =>
    setTimeout(() => {
        res("Hello, world!");
    }, 1000)
);

async function q1() {
    var res = await p1();
    console.log(res);
}

async function q2() {
    var res = await p1();
    console.log(res);
}

q1(); 
q2(); 
// 1s之后输出(同时输出):
// Hello, world!
// Hello, world!

编写第一个async/await函数

原始代码(在Chrome浏览器中运行):

function getFetchReq() {
    fetch('https://jsonplaceholder.typicode.com/posts', {})
    .then((data) => {
        console.log(`Status: ${data.status}`);
        console.log(`Url: ${data.url}`);
    });
}

getFetchReq();

修改之后代码:

async function getFetchReq() {
    const url = 'https://jsonplaceholder.typicode.com/posts';
    const resp = await fetch(url, {});
    console.log(`Status: ${resp.status}`);
    console.log(`Url: ${resp.url}`);
}

getFetchReq();

将async函数用在promisechain中

async function getFetchReq() {
    const url = 'https://jsonplaceholder.typicode.com/posts';
    return await fetch(url, {});
}

class APIClient {
    async getStatusAndResp() {
        const url = 'https://jsonplaceholder.typicode.com/posts';
        return await fetch(url, {});
    }
}

(async () => {
    const client = new APIClient();
    const resp = await client.getStatusAndResp();
    console.log(`Status: ${resp.status}`);
    console.log(`Url: ${resp.url}`);
})();

async/await 中错误处理

(1)方式一:try-catch 来错误捕获

async function myFunction() {
    try {
        await Promise.reject("1");
    } catch (err) {
        console.error(err);
    }
}

myFunction(); // "1"

(2)方式二:Promise中catch来错误捕获

async function myFunction() {
    await Promise.reject("1")
        .catch((err) => {
            console.error(err);
        });
}

myFunction(); // "1"

正确处理多个await操作的并行串行

  • 串行运行
const sleep = (timeout = 2000) => new Promise(resolve => {
    setTimeout(resolve, timeout);
});

async function getFetchReq() {
    await sleep();
    const url = 'https://jsonplaceholder.typicode.com/posts';
    return await fetch(url, {});
}

const showRespInfo = async () => {
    console.time(`showRespInfo`);
    const resp1 = await getFetchReq();
    const resp2 = await getFetchReq();

    console.log(`Status: ${resp1.status}`);
    console.log(`Url: ${resp1.url}`);

    console.log(`Status: ${resp2.status}`);
    console.log(`Url: ${resp2.url}`);

    console.timeEnd(`showRespInfo`);
};

showRespInfo();

运行结果:

Status: 200
Url: https://jsonplaceholder.typicode.com/posts
Status: 200
Url: https://jsonplaceholder.typicode.com/posts
showRespInfo: 4904.76513671875 ms
  • 并行运行
const sleep = (timeout = 2000) => new Promise(resolve => {
    setTimeout(resolve, timeout);
});

async function getFetchReq() {
    await sleep();
    const url = 'https://jsonplaceholder.typicode.com/posts';
    return await fetch(url, {});
}

const showRespInfo = async () => {
    console.time(`showRespInfo`);
    const resp1Promise = getFetchReq();
    const resp2Promise = getFetchReq();
    const resp1 = await resp1Promise;
    const resp2 = await resp2Promise;

    console.log(`Status: ${resp1.status}`);
    console.log(`Url: ${resp1.url}`);

    console.log(`Status: ${resp2.status}`);
    console.log(`Url: ${resp2.url}`);

    console.timeEnd(`showRespInfo`);
};

showRespInfo();

运行结果:

Status: 200
Url: https://jsonplaceholder.typicode.com/posts
Status: 200
Url: https://jsonplaceholder.typicode.com/posts
showRespInfo: 2900.43505859375 ms

使用Promis.all()让多个await操作并行

  • 并行运行
const sleep = (timeout = 2000) => new Promise(resolve => {
    setTimeout(resolve, timeout);
});

async function getFetchReq() {
    await sleep();
    const url = 'https://jsonplaceholder.typicode.com/posts';
    return await fetch(url, {});
}

const showRespInfo = async () => {
    console.time(`showRespInfo`);
    const [resp1, resp2] = await Promise.all([
        getFetchReq(), getFetchReq()
    ]);

    console.log(`Status: ${resp1.status}`);
    console.log(`Url: ${resp1.url}`);

    console.log(`Status: ${resp2.status}`);
    console.log(`Url: ${resp2.url}`);

    console.timeEnd(`showRespInfo`);
};

showRespInfo();

运行结果:

Status: 200
Url: https://jsonplaceholder.typicode.com/posts
Status: 200
Url: https://jsonplaceholder.typicode.com/posts
showRespInfo: 3087.52197265625 ms

结合await和任意兼容.then()的代码

async function main() {
    const number = 800;
    console.log(number);
}

main();

等价于:

async function main() {
    const number = await Promise.resolve(800);
    console.log(number);
}

main();

在for循环中正确使用await

  • 串行运行
const sleep = (timeout = 2000) => new Promise(resolve => {
    setTimeout(resolve, timeout);
});

async function getFetchReq() {
    await sleep();
    const url = 'https://jsonplaceholder.typicode.com/posts';
    return await fetch(url, {});
}

const showRespInfo = async () => {
    console.time(`showRespInfo`);

    for (let i = 0; i < 2; i++) {
        let resp = await getFetchReq();
        console.log(`Status: ${resp.status}`);
        console.log(`Url: ${resp.url}`);
    }

    console.timeEnd(`showRespInfo`);
}

showRespInfo();

运行结果:

Status: 200
Url: https://jsonplaceholder.typicode.com/posts
Status: 200
Url: https://jsonplaceholder.typicode.com/posts
showRespInfo: 4543.48388671875 ms

async/await 和 promise 的区别

promise最大的问题就是在于业务复杂之后,then内部的逻辑也变得复杂,或者循环的异步嵌套场景等,会写出来不那么优美。

function who() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('🤡');
    }, 200);
  });
}

function what() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('lurks');
    }, 300);
  });
}

function where() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('in the shadows');
    }, 500);
  });
}

async function msg() {
  const a = await who();
  const b = await what();
  const c = await where();

  console.log(`${ a } ${ b } ${ c }`);
}

msg(); // 🤡 lurks in the shadows <-- after 1 second

在上面的示例中,每个步骤都是按顺序完成的,每个其他步骤都需要先解决该步骤,然后再拒绝,然后再继续。相反,如果您希望这些步骤并行进行,则只需使用Promise.all等待所有诺言都兑现:

function who() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('🤡');
        }, 200);
    });
}

function what() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('lurks');
        }, 300);
    });
}

function where() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('in the shadows');
        }, 500);
    });
}

async function msg() {
    const [a, b, c] = await Promise.all([who(), what(), where()]);

    console.log(`${a} ${b} ${c}`);
}

msg(); // 🤡 lurks in the shadows <-- after 1 second

等价于:

function who() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('🤡');
        }, 200);
    });
}

function what() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('lurks');
        }, 300);
    });
}

function where() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('in the shadows');
        }, 500);
    });
}

Promise.all([who(), what(), where()]).then(function (a) {
    console.log(`${a}`); // 🤡 lurks in the shadows <-- after 1 second
});

4、正则表达式

4.1 正则表达式举例说明

1)判断输入是否为数字、字母、下划线组成

var isValid = function(str) {
    return /^\w+$/.test(str);      // '^'表示从字符串开头开始,'+$'表示到字符串结束
};

var str1 = "1234abd__";
console.log("'1234abd__' isValid? " + isValid(str1));
var str2 = "$32343";
console.log("'$32343' isValid? " + isValid(str2));


// 输出结果:
// '1234abd__' isValid? true
// '$32343' isValid? false

5、面向对象介绍

面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。它使用先前建立的范例,包括模块化,多态和封装几种技术。今天,许多流行的编程语言(如Java,JavaScript,C#,C+ +,Python,PHP,Ruby和Objective-C)都支持面向对象编程(OOP)。

面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性,在大型软件工程中广为流行。凭借其对模块化的重视,面向对象的代码开发更简单,更容易理解,相比非模块化编程方法,它能更直接地分析,编码和理解复杂的情况和过程。

5.1 术语

Namespace 命名空间
  允许开发人员在一个独特, 应用相关的名字的名称下捆绑所有功能的容器。
  Class 类
  定义对象的特征。它是对象的属性和方法的模板定义.
  Object 对象
  类的一个实例。
  Property 属性
  对象的特征,比如颜色。
  Method 方法
  对象的能力,比如行走。
  Constructor 构造函数
  对象初始化的瞬间, 被调用的方法. 通常它的名字与包含它的类一致.
  Inheritance 继承
  一个类可以继承另一个类的特征。
  Encapsulation 封装
  一种把数据和相关的方法绑定在一起使用的方法.
  Abstraction 抽象
  结合复杂的继承,方法,属性的对象能够模拟现实的模型。
  Polymorphism 多态
  多意为‘许多’,态意为‘形态’。不同类可以定义相同的方法或属性。

更多关于面向对象编程的描述,请参照维基百科的 面向对象编程 。

6、继承与原型链

6.1 继承与原型链(MDN)

举例:

// 1. 语法结构创建的对象
// 创建一个自身拥有属性a和b的函数里创建一个对象o
function f() {
    this.a = 1;
    this.b = 2;
}

var o = new f();
// 在f函数的原型上定义属性
f.prototype.c = 3;
f.prototype.d = 3;

f.prototype.func = function () {
    console.log("1.5 hello");
}

console.log(f);
console.log(JSON.stringify(f.prototype)); // {"c":3,"d":5}
console.log(f.prototype.prototype); // undefined
console.log("1.4 o.a: " + o.a + ", o.b: " + o.b + ", o.c: " + o.c + ", o.d: " + o.d); // 1.4 o.a: 1, o.b: 2, o.c: 3, o.d: 5
o.func(); // 1.5 hello

var o1 = {
    a: 2,
    m: function () {
        return this.a + 2;
    }
}
console.log(o1.m()); // 当调用 o.m 时,"this" 指向了 o1, 4

var p = Object.create(o1); // p 是一个继承来自 o1 的对象
p.a = 4;
console.log("1.7 " + p.m());
console.log("1.8 " + Object.prototype + "\n");

// 2. 构造器创建的对象
function Graph() {
    this.vertices = [];
    this.edges = [];
}
Graph.prototype = {
    addVertex: function (v) {
        this.vertices.push(v);
    }
};

var g = new Graph();
g.addVertex(15);
console.log("2.1 g.vertices[0]: " + g.vertices[0] + "\n");

// 3. Object.create创建的对象
var aa = {
    a1: 1
};
// aa ---> Object.prototype ---> null
console.log("3.1 " + aa.hasOwnProperty);
var b = Object.create(aa); // 创建一个新的对象
// b ---> aa ---> Object.prototype ---> null
console.log("3.2 " + b.hasOwnProperty);
var c = Object.create(b); // 创建一个新的对象
// c ---> b ---> aa ---> Object.prototype ---> null
console.log("3.3 " + c.hasOwnProperty);
var d = Object.create(null); // 创建一个新的对象
// d ---> null
console.log("3.4 " + d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype

// 4. class 关键字创建的对象
"use strict";

class Polygon {
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
}

class Square extends Polygon {
    constructor(sideLength) {
        super(sideLength, sideLength);
    }
    get area() {
        return this.height * this.width;
    }
    set sideLength(newLength) {
        this.height = newLength;
        this.width = newLength;
    }
    call() {
        console.log("4.2 hello");
    }
}

var square = new Square(2);
console.log("4.1 square.height: " + square.height + ", square.width: " + square.width + "\n");

// 5. 性能
// 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。
// 遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。
// 要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从Object.prototype继承的 hasOwnProperty 方法。
console.log("5.1 " + square.hasOwnProperty("height"));
console.log("5.2 " + square.hasOwnProperty("height2"));
// hasOwnProperty 是 JavaScript 中处理属性并且不会遍历原型链的方法之一。(另一种方法: Object.keys())
// 注意:检查属性是否undefined还不够。该属性可能存在,但其值恰好设置为undefined。

6.2 继承

1)伪类

当一个函数对象被创建时,Function构造器产生的函数对象会运行类似这样的一些代码:

this.prototype = { constructor: this };

新函数对象被赋予一个prototype属性,其值是包含一个constructor属性且属性值为该新函数对象。该prototype对象是存放继承特征的地方。因为JavaScript语言没有提供一种方法确定哪个函数式打算用来作构造器的,所以每个函数都会得到一个prototype对象。constructor属性没什么用。重要的是prototype对象。

我们可以定义一个构造器并扩充它的原型:

var Mammal = function (name) {
    this.name = name;
};
Mammal.prototype.get_name = function () {
    return this.name;
};
Mammal.prototype.says = function (val) {
    console.log(val);
};

// 现在可以构造一个实例
var myMammal = new Mammal("Herb the Mammal");
var name = myMammal.get_name(); // Herb David
myMammal.says("Hello world"); // Hello world



var Cat = function (name) {
    this.name = name;
    this.saying = 'meow';
};
// 替换Cat.prototype为一个新的Mammal实例
Cat.prototype = new Mammal();
// 扩充新原型对象,增加purr和get_name方法
Cat.prototype.purr = function (n) {
    var i, s = '';
    for (i = 0; i < n; i++) {
        if (s) {
            s += '-';
        }
        s += 'r';
    }
    return s;
};
Cat.prototype.get_name = function () {
    return this.says("Hello world") + ' ' + this.name + ' ' + this.says("Hello world");
};

var myCat = new Cat('Henrietta');
var says = myCat.says();
console.log(says); // 'Hello world'
var purr = myCat.purr(5);
console.log(purr); // r-r-r-r-r
var name = myCat.get_name();
console.log(name); // 'Hello world Henrietta Hello world'

2)对象说明符(属性特性)

有时候,构造器要接受一大串的参数。这可能是令人烦恼的,因为要记住参数的顺序可能非常困难。在这种情况下,如果我们在编写构造器时使其接受一个简单的对象说明符可能会更加友好。那个对象包含了将要构建的对象规格说明。所以,语气这样写:

var myObject = maker(f, l, m, c, s);

不如这么写:

var myObject = maker({
    first: f,
    last: l,
    state: s,
    city: c
});

现在多个参数可以任意顺序排列,如果构造器会聪明地使用默认值,一些参数可以忽略掉,并且代码也更容易阅读。

先创建一个对象:

var person = {
    name: "Nicholas",
    _job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    },
    get job() {
        return this._job;
    },
    set job(newJob) {
        this._job = newJob;
    }
};

console.log(person.name); // 输出Nicholas
console.log(person._job); // "Software Engineer"
console.log(person.job); // "Software Engineer"
person.job = 'Coder';
console.log(person.job); // Coder
console.log(person._job); // Coder

在这个对象中,我们定义了一个name属性和一个_job属性;至于set和get开头的两处代码,它们共同定义了一个属性job。明显属性job和_job、name的属性是不同的。是的,JavaScript中的对象由两种不同类型的属性:数据属性和访问器属性。name和_job是数据属性,job是访问器属性数据属性和访问器属性最大的不同在于:当访问一个访问器属性时,得到get后面函数的返回值;给访问器属性赋值时,执行的是set后面的函数,这个函数以赋的值为参数。

上述代码中,在set函数中我们通过this改变了_job的值(this指向person这个对象)。

我们在创建person对象时没有为它的属性们直接指定特征值,JavaScript自动为它们创建了属性特性。在ES3中属性特性不可访问,但是ES5中属性的特性可以通过Object.getOwnPropertyDescriptors或Object.getOwnPropertyDescriptor得到:

var descriptors= Object.getOwnPropertyDescriptors(person);
console.log(descriptors);

输出结果如下(descriptors的属性):

{
    name:
    {
        value: 'Nicholas',
        writable: true,
        enumerable: true,
        configurable: true
    },
    _job:
    {
        value: 'Coder',
        writable: true,
        enumerable: true,
        configurable: true
    },
    sayName:
    {
        value: [Function: sayName],
        writable: true,
        enumerable: true,
        configurable: true
    },
    job:
    {
        get: [Function: get job],
        set: [Function: set job],
        enumerable: true,
        configurable: true
    }
}

(1)数据属性

数据属性的描述符的有四个属性分别是:

  1. value:包含这个属性的数据值。读取属性的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认为undefined。
  2. writable:表示能否修改属性的值。是一个bool值,默认为true。
  3. enumerable:属性是否可枚举,即能否通过for-in循环返回属性。是一个bool值,默认为true。
  4. configrable:属性是否可配置。即属性能否通过delete删除,能否修改属性的特性,或者能否把属改为访问器属性。是一个bool值,默认为true。

我们最初开始创建的person对象的属性name,它的value为“Nicholas”,其他描述符为true。

var person = {};
// 除了configrable之外,其他三个属性相互之间不会影响,读者可以自己测试

console.log('---------------------1 writable start---------------------');
Object.defineProperty(person, "name", {
    writable: false,
    enumerable: true,
    configurable: true,
    value: "Nicholas"
});

console.log("1.1 " + person.name); // "Nicholas"
person.name = "Greg";
// writable为false,属性不可修改
console.log("1.2 " + person.name); // "Nicholas"

// writable为false,但configrable为true,我们可以重新配置属性描述符,
Object.defineProperty(person, "name", {
    writable: false,
    enumerable: true,
    configurable: true,
    value: "John"
});
console.log("1.3 " + person.name); // John

delete person.name
//但configrable为true,属性可以被删除
console.log("1.4 " + person.name); // undefined 
console.log('---------------------1 writable end---------------------\n');

console.log('---------------------2 enumerable start---------------------');
var person = {};
Object.defineProperty(person, "name", {
    writable: false,
    enumerable: true,
    configurable: true,
    value: "Nicholas"
});
// enumerable为true属性可枚举
for (var prop in person) {
    console.log("2.1 " + prop); // name
}

Object.defineProperty(person, "name", {
    writable: false,
    enumerable: false,
    configurable: true,
    value: "Nicholas"
});
// enumerable为false属性不可枚举,循环体不执行
for (var prop in person) {
    console.log("2.2 " + prop);
}
console.log('---------------------2 enumerable end---------------------\n');

console.log('---------------------3 configurable start---------------------');
var person = {};
Object.defineProperty(person, "name", {
    writable: true,
    enumerable: true,
    configurable: false,
    value: "Nicholas"
});
// configurable为false,writable为true,属性仍然可修改
person.name = "John"
console.log("3.1 " + person.name); // John

// configurable为false,writable为true,仍然可以通过配置的方式改变属性值
Object.defineProperty(person, "name", {
    writable: true,
    enumerable: true,
    configurable: false,
    value: "Nicholas"
});
console.log("3.2 " + person.name); // Nicholas

// configurable为false,enumerable为ture,属性可枚举
for (var prop in person) {
    console.log("3.3 " + prop); // name
}

// configurable为false,我们仍然可以把writable属性由true改为false
Object.defineProperty(person, "name", {
    writable: false,
    enumerable: true,
    configurable: false,
    value: "Nicholas"
});
console.log("3.4 " + Object.getOwnPropertyDescriptor(person, "name")); // {value: "Nicholas", writable: false, enumerable: true, configurable: false}

// configurable为false,writable为false,不能通过配置改变value的值
try {
    Object.defineProperty(person, "name", {
        writable: false,
        enumerable: true,
        configurable: false,
        value: "John"
    });
} catch (error) {
    console.log("3.5 " + "value change error");
    console.log(Object.getOwnPropertyDescriptor(person, "name")); // {value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

// configurable为false,但是不能把writable属性由false改为true
try {
    Object.defineProperty(person, "name", {
        writable: true,
        enumerable: true,
        configurable: false,
        value: "Nicholas"
    });
} catch (error) {
    console.log("3.6 " + "writable false to true error")
    console.log(Object.getOwnPropertyDescriptor(person, "name")); // {value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

// configurable为false,不能改变enumerable的值
try {
    Object.defineProperty(person, "name", {
        writable: false,
        enumerable: false,
        configurable: false,
        value: "Nicholas"
    });
} catch (error) {
    console.log("3.7 " + "enumerable change error");
    console.log(Object.getOwnPropertyDescriptor(person, "name")); // {value: "Nicholas", writable: false, enumerable: true, configurable: false}
}

var person = {};
Object.defineProperty(person, "name", {
    writable: true,
    enumerable: true,
    configurable: true,
    value: "Nicholas"
});

// configurable为true,可以把数据属性修改为访问器属性
try {
    Object.defineProperty(person, "name", {

        get: function () { return "Nicholas" },
        enumerable: true,
        configurable: false
    });
} catch (error) {
    console.log("3.8 " + "get error");
}
console.log("3.9 " + Object.getOwnPropertyDescriptor(person, "name")); // {set: undefined, enumerable: true, configurable: false, get: ƒ}

var person = {};
Object.defineProperty(person, "name", {
    writable: true,
    enumerable: true,
    configurable: false,
    value: "Nicholas"
});
// configurable为false,不可以把数据属性修改为访问器属性
try {
    Object.defineProperty(person, "name", {
        get: function () { return "Nicholas" },
        enumerable: true,
        configurable: false
    });
} catch (error) {
    console.log("3.10 " + "get error");
}
console.log("3.11 " + Object.getOwnPropertyDescriptor(person, "name")); // {value: "Nicholas", writable: true, enumerable: true, configurable: false}
console.log('---------------------3 configurable end---------------------\n');

(2)访问器属性

访问器属性的描述符的有四个属性分别是:

  1. get:在读取属性时调用的函数。默认值为 undefined。
  2. set:在写入属性时调用的函数。默认值为 undefined。
  3. enumerable:属性是否可枚举,即能否通过for-in循环返回属性。是一个bool值,默认为true。
  4. configrable:属性是否可配置。即属性能否通过delete删除,能否修改属性的特性,或者能否把属性修改为数据属性。是一个bool值,默认为true。

我们最初开始创建的person对象的属性job,它的get和set值分别是我们指定的函数,其他描述符为true。

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty或 Object.defineProperties。

有了定义属性特性的方法,那我们通过代码来探索下这些属性特性的作用:

console.log("----------------------1 访问器属性开始----------------------");
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
    get: function () {
        console.log("1.1 get被调用");
        return this._name;
    },
    set: function (newName) {
        console.log("1.2 set被调用");
        this._name = newName;
    },
    enumerable: true,
    configurable: true
});
person.name; // get被调用
person.name = "John"; // set被调用
console.log("----------------------1 访问器属性结束----------------------\n");

console.log("----------------------2 不设set开始----------------------");
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
  get: function () {
            console.log("2.1 get被调用");
            return this._name;
        },
    enumerable: true,
    configurable: true
});
person.name; // get被调用
person.name = "John"; // 没有设置set,什么也没发生
console.log("2.2 " + person.name) // Nicholas
console.log("----------------------2 不设set结束----------------------\n");

console.log("----------------------3 不设get开始----------------------");
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
    set: function (newName) {
        console.log("3.1 set被调用");
        this._name = newName;
    },
    enumerable: true,
    configurable: true
});
console.log("3.2 " + person.name); // 没有get,得到 undefined
console.log("3.3 " + person._name); // Nicholas
person.name = "John"; // set被调用
console.log("3.4 " + person._name) // John,通过set,_name的值被改变
console.log("----------------------3 不设get结束----------------------\n");

console.log("----------------------4 不设get set开始----------------------");
// 虽然不报错,但是这个属性没有任何意义
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
    enumerable: true,
    configurable: true
});
console.log("----------------------4 不设get set结束----------------------\n");

console.log("----------------------5 enumerable 开始----------------------");
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
    get: function () {
        console.log("5.1 " + "get被调用");
        return this._name;
    },
    set: function (newName) {
        console.log("5.2 " + "set被调用");
        this._name = newName;
    },
    enumerable: true,
    configurable: true
});
for (var prop in person) {
    console.log("5.3 " + prop); // _name,name
}

Object.defineProperty(person, "name", {
    get: function () {
        console.log("5.4 " + "get被调用");
        return this._name;
    },
    set: function (newName) {
        console.log("5.5 " + "set被调用");
        this._name = newName;
    },
    enumerable: false,
    configurable: true
});
for (var prop in person) {
    console.log("5.6 " + prop); // _name
}
console.log("----------------------5 enumerable 结束----------------------\n");

console.log("----------------------6 configurable 开始----------------------");
var person = {_name: "Nicholas"};
Object.defineProperty(person, "name", {
    get: function () {
        console.log("6.1 " + "get被调用");
        return this._name;
    },
    set: function (newName) {
        console.log("6.2 " + "set被调用");
        this._name = newName;
    },
    enumerable: true,
    configurable: false
});
person.name; // get被调用
person.name = "John"; // set被调用
console.log("6.3 " + person.name); // John

// configurable: false,重新配置访问器属性报错
try{
Object.defineProperty(person, "name", {
    get: function () {
        console.log("6.4 " + "get被调用");
        return this._name;
    },
    set: function (newName) {
        console.log("6.5 " + "set被调用");
        this._name = newName;
    },
        enumerable: true,
        configurable: false
    });
} catch (e) {
    console.log("6.6 " + "不能重新定义name的属性标识符");
}

// configurable: false,重新配置数据属性报错
try{
    Object.defineProperty(person, "name", {
        value: "123",
        writable: true,
        enumerable: true,
        configurable: false
    });
} catch (e) {
  console.log("6.7 " + "不能重新定义name的属性标识符");
}
console.log("----------------------6 configurable 结束----------------------\n");

(3)可扩展性

ES5定义了三个方法Object.preventExtensions、Object.seal、Object.freeze分别定义了不同级别的可扩展性。可点击连接前往MDN阅读。

(4)最后:get、set 与继承

function Person () {}

var p = Person.prototype;
p._name = "John";
Object.defineProperty(p, "name", {
    get: function () {return this._name;},
    set: function (newName) {this._name = newName;},
    enumerable: true,
    configurable: true
});

var person = new Person();
console.log("1 " + person.name); // John
console.log("2 " + person.hasOwnProperty("_name")); // false
console.log("3 " + person.hasOwnProperty("name")); // false

// 虽然name属性的set get方法是定义在原型中的
// 但是通过person调用时,它们的this属性会指向person
// 所以通过person.name设置属性时,执行set方法中this._name = newName时
// 会给person对象添加_name属性,并把newName赋给person新建的属性_name
person.name = "Nicholas";
console.log("4 " + person.name); // Nicholas
console.log("5 " + p._name); // John
console.log("6 " + person.hasOwnProperty("name")); // false
console.log("7 " + person.hasOwnProperty("_name")); // true

3)函数化

函数化构造器的伪代码模板:

// 函数化构造器的伪代码模板
var constructor = function (spec, my) {
    var that, 其它的私有实例变量;
    my = my || {};
    // 把共享的变量和函数添加到my中
    that = 一个新对象;
    添加给that的特权方法
    return that;
};

上述模板中,spec对象包含构造器要构造一个新实例的所有信息。spec的内容可能会被复制到私有变量中,或者被其它函数改变。或者方法可以在需要的时候访问spec的信息(一个简化的方式是替代spec为一个单一的值。当构造对象时并不需要整个spec的时候,这是有用的)。my对象是一个为伪继承链中的构造器提供秘密共享的容器。my对象可以选择性地使用。如果没有传入一个my对象,那么会创建一个my对象。

var mammal = function (spec) {
    var that = {};
    that.get_name = function () {
        return spec.name;
    };
    that.says = function () {
        return spec.saying || '';
    };
    return that;
};
var myMammal_1 = mammal({ name: 'Herb' });
console.log("1.1 " + myMammal_1.get_name()); // Herb
console.log("1.2 " + myMammal_1.says());

var myMammal_2 = mammal({ name: 'Herb', saying: 'Hello, world!' });
console.log("2.1 " + myMammal_2.get_name()); // Herb
console.log("2.2 " + myMammal_2.says()); // Hello, world!

// 在伪类模式中,构造器函数Cat不得不重复构造器Mammal已经完成的工作。
// 在函数话模式中那不再需要了,因为构造器将会调用构造器Mammal,
// 让Mammal去做创建对象中的大部分工作,所以Cat只需关注自身的差异即可。
var cat = function (spec) {
    spec.saying = spec.saying || 'meow';
    var that = mammal(spec);
    that.purr = function (n) {
        var i, s = '';
        for (i = 0; i < n; i++) {
            if (s) {
                s += '-';
            }
            s += 'r';
        }
        return s;
    };
    that.get_name = function () {
        return this.says() + " " + spec.name + " " + this.says();
    };
    return that;
};
var myCat = cat({ name: 'Henrietta' });
console.log("3.1 " + myCat.get_name()); // meow Henrietta meow
console.log("3.2 " + myCat.purr(5)); // r-r-r-r-r

7、严格模式

目的:

  • 消除JavaScript语法的一些不合理/不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全指出,为代码的安全运行保驾护航;
  • 为未来新版本的JavaScript做好铺垫。

ECMAScript 5的严格模式是采用具有限制性JavaScript变体的一种方式,从而使代码显示地 脱离“马虎模式/稀松模式/懒散模式“(sloppy)模式。

严格模式不仅仅是一个子集:它的产生是为了形成与正常代码不同的语义。

不支持严格模式与支持严格模式的浏览器在执行严格模式代码时会采用不同行为。

所以在没有对运行环境展开特性测试来验证对于严格模式相关方面支持的情况下,就算采用了严格模式也不一定会取得预期效果。严格模式代码和非严格模式代码可以共存,因此项目脚本可以渐进式地采用严格模式。

严格模式对正常的 JavaScript语义做了一些更改。

  1. 严格模式通过抛出错误来消除了一些原有静默错误。
  2. 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。
  3. 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。

7.1 为脚本开启严格模式

为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 "use strict"; (或 'use strict';)

// 整个脚本都开启严格模式的语法
"use strict";
var v = "Hi!  I'm a strict mode script!";

这种语法存在陷阱,有一个大型网站已经被它坑倒了:不能盲目的合并冲突代码。试想合并一个严格模式的脚本和一个非严格模式的脚本:合并后的脚本代码看起来是严格模式。反之亦然:非严格合并严格看起来是非严格的。合并均为严格模式的脚本或均为非严格模式的都没问题,只有在合并严格模式与非严格模式有可能有问题。建议按一个个函数去开启严格模式(至少在学习的过渡期要这样做).

您也可以将整个脚本的内容用一个函数包括起来,然后在这个外部函数中使用严格模式。这样做就可以消除合并的问题,但是这就意味着您必须要在函数作用域外声明一个全局变量。

7.2 为函数开启严格模式

同样的,要给某个函数开启严格模式,得把 "use strict"; (或 'use strict'; )声明一字不漏地放在函数体所有语句之前。

function strict() {
    // 函数级别严格模式语法
    'use strict';
    function nested() { return "And so am I!"; }
    return "Hi!  I'm a strict mode function!  " + nested();
}
function notStrict() { return "I'm not strict."; }

8、模块化

8.1 ES6 Module的使用(import/export)

1)Module基本使用

  • export命令:用于规定模块对外的接口
    • 外部能够读取模块内部的某个变量、函数、类
    • 使用as关键字重命名
    • 读命令可以出现在模块的任何位置,只要处于模块顶层即可,除了块作用域内(import也是如此)
  • import命令:用于输入其它模块提供的扩展
    • 变量、函数
    • 使用as关键字重命名
    • 输入的变量都是只读的
    • import命令具有提升效果(即:可以先使用变量,然后import变量的来源)

举例1:

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> -->
    <script type='module' src='import.js'></script>
</head>

<body>
    <!-- 
        Safari 10.1.
        谷歌浏览器(Canary 60) – 需要在chrome:flags里开启: "实验性网络平台功能(Experimental Web Platform)"
        Firefox 54 – 需要在about:config里开启dom.moduleScripts.enabled选项
        Edge 15 – 需要在about:flags里开启 Experimental JavaScript Features 选项
    -->
    <!-- 
        export.js: 公共的可复用的内容
        import.js
        index.html: 引入 import.js 内容
     -->
</body>

</html>

export.js

// export.js 用于导入变量和函数
// 1.1 导出变量方式一
export let name = "David";
export let age = 18;

// 1.3 导出变量(对象),先定义变量,后export
export let obj1 = {
    name: "John",
    age: 22
};

let obj2 = {
    name: "John2",
    age: 22
};
export { obj2 as newObj2 };

// 2.1 导出函数
export function addFn(a, b) {
    console.log(a + b);
}

// 2.2 导出多个函数
function fn1() {
    console.log("fn1");
}
function fn2() {
    console.log("fn2");
}

export { fn1 as tempFn1, fn2 as tempFn2 }

import.js

// import.js 用于导出变量和函数
// 1.1 导入变量,导入的变量是只读的,不可修改的
import { name, age } from './export.js'
console.log(`1.1 My name is ${name}, age is ${age}`); // My name is David, age is 18

// age = 30; // Error: 0: Assignment to const

// 1.2 导入变量,可以给变量取别名
import { name as myName, age as myAge } from './export.js'
console.log(`1.2 My name is ${myName}, age is ${myAge}`); // My name is David, age is 18

// 1.3 导入变量(对象),可以修改变量中的内容
import { obj1 } from './export.js'
console.log(obj1); // [object Object]: {age: 22, name: "John"}
obj1.age = 25;
console.log(obj1); // [object Object]: {age: 25, name: "John"}


import { newObj2 } from './export.js'
console.log(newObj2); // [object Object]: {age: 22, name: "John2"}

// 2.1 导入函数
import { addFn as add } from './export.js'
add(22, 23); // 45

// 2.2 导入多个函数
import { tempFn1, tempFn2 } from './export.js'
tempFn1(); // fn1
tempFn2(); // fn2


// module 是静态导入
// 不能使用表达式和变量那些运行时才能看到结果的代码
/* 
// 举例1
let path = './export.js'
import tempFn2 from path // Error: SCRIPT5022: SCRIPT5022: Let/Const redeclaration
 */

/* 
// 举例2
if (1 == 1) {
    import { addFn as add } from './export.js'; // Error: SCRIPT5022: SCRIPT5022: Module import or export statement unexpected here
} */

2)Module其它使用

  • Module的语法-模块整体加载。

  • Module的语法-export default命令: 为模块指定默认输出。

  • Module的语法-export和import的复合写法: 如果在一个模块中,先输入后输出一个模块,import语句和export语句可以写在一起。

举例1(Module的语法-模块整体加载)

用星号(*)指定一个对象,所有输出值都加载在这个对象上。

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> -->
    <script type='module' src='import.js'></script>
</head>

<body>
    <!-- 
        Safari 10.1.
        谷歌浏览器(Canary 60) – 需要在chrome:flags里开启: "实验性网络平台功能(Experimental Web Platform)"
        Firefox 54 – 需要在about:config里开启dom.moduleScripts.enabled选项
        Edge 15 – 需要在about:flags里开启 Experimental JavaScript Features 选项
    -->
    <!-- 
        export.js: 公共的可复用的内容
        import.js
        index.html: 引入 import.js 内容
     -->
</body>

</html>

export.js

import * as obj from './export.js'

console.log(obj.name); // David
console.log(obj.age); // 18

console.log(obj.obj1); // [object Object]: {age: 22, name: "John"}
console.log(obj.newObj2); // [object Object]: {age: 22, name: "John2"}

obj.addFn(22, 23); // 45

obj.tempFn1(); // fn1
obj.tempFn2(); // fn2

import.js

// export.js 用于导入变量和函数
// 1.1 导出变量方式一
export let name = "David";
export let age = 18;

// 1.3 导出变量(对象),先定义变量,后export
export let obj1 = {
    name: "John",
    age: 22
};

let obj2 = {
    name: "John2",
    age: 22
};
export { obj2 as newObj2 };

// 2.1 导出函数
export function addFn(a, b) {
    console.log(a + b);
}

// 2.2 导出多个函数
function fn1() {
    console.log("fn1");
}
function fn2() {
    console.log("fn2");
}

export { fn1 as tempFn1, fn2 as tempFn2 }

举例2(Module的语法-export default命令)

注意:一个JS模块中,只能由一个 export default。

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> -->
    <script type='module' src='import.js'></script>
</head>

<body>
    <!-- 
        Safari 10.1.
        谷歌浏览器(Canary 60) – 需要在chrome:flags里开启: "实验性网络平台功能(Experimental Web Platform)"
        Firefox 54 – 需要在about:config里开启dom.moduleScripts.enabled选项
        Edge 15 – 需要在about:flags里开启 Experimental JavaScript Features 选项
    -->
    <!-- 
        export.js: 公共的可复用的内容
        import.js
        index.html: 引入 import.js 内容
     -->
</body>

</html>

export.js

// export.js 用于导入变量和函数
// 1.1 默认导出,变量
// export default function () {
//     console.log("This is a export default test.");
// }

// 1.2 默认导出,类
export default class Person {
    constructor(name, age) {
        this.name = 'David';
        this.age = 15;
    }
}

import.js

// import myFun from './export.js'
import Person from './export.js'
// 注意:
//  正常输出,导入时加 {}
//  使用default导出,导入时不要加 {}

// 1.1 导入,变量
// myFun(); // This is a export default test.

// 1.2 导入,类
var p = new Person();
console.log(p.name); // David
console.log(p.age); // 15

举例3(Module的语法-export和import的复合写法)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> -->
    <script type='module' src='import.js'></script>
</head>

<body>
    <!-- 
        Safari 10.1.
        谷歌浏览器(Canary 60) – 需要在chrome:flags里开启: "实验性网络平台功能(Experimental Web Platform)"
        Firefox 54 – 需要在about:config里开启dom.moduleScripts.enabled选项
        Edge 15 – 需要在about:flags里开启 Experimental JavaScript Features 选项
    -->
    <!-- 
        export.js: 公共的可复用的内容
        import.js
        index.html: 引入 import.js 内容
     -->
</body>

</html>

public.js

export let name = "Name from public.js";

export.js

// export.js 用于导入变量和函数
// 1.1 导出变量方式一
export { name } from './public.js'

// 等价于
// import { name } from './public.js'
// export { name }

import.js

import { name } from './export.js'
console.log(`Name is '${name}'`); // Name is 'Name from public.js'

8.2 CommonJS(require)的使用

CommonJS API定义很多普通应用程序(主要指非浏览器的应用)使用的API。

CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。

举例1(单个导出导入情况)

export.js

function add(a, b) {
    console.log(a + b);
}

module.exports = add;

import.js

const add = require('./export');

add(1, 2); // 3

举例2(多个导出导入情况)

export.js

function add(a, b) {
    console.log(a + b);
}

function func2() {
    console.log("Hello, world!");
}

module.exports = { add, func2 };
// 等价于:
// module.exports = { add: add, func2: func2 };

import.js

const { add, func2 } = require('./export');
add(1, 2);
func2();

举例3(整体导出导入情况)

export.js

function add(a, b) {
    console.log(a + b);
}

function func2() {
    console.log("Hello, world!");
}

module.exports = { add, func2 };
// 等价于:
// module.exports = { add: add, func2: func2 };

import.js

const X = require('./export');
X.add(1, 2);
X.func2();

8.3 AMDJS的使用

AMD专门用于浏览器端,模块的加载是异步的。

AMD 的诞生,就是为了解决这两个问题:

  1. 实现 js 文件的异步加载,避免网页失去响应;
  2. 管理模块之间的依赖性,便于代码的编写和维护。

实现 AMD 规范的加载器其实是挺多的,目前,主要有两个 Javascript 库实现了 AMD 规范:require.js 和 curl.js。不过多数人还是用 require.js(先定义所有依赖,然后在加载完成后的回调函数中执行)。

define() 函数,函数描述为:

define(id?, dependencies?, factory);
  • id: 它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
  • dependencies: 是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
  • factory: 为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

require()函数,函数描述为:

require([module], callback)

定义暴露模块:

  • 定义一个没有依赖的模块

    define(funcion() {
    	return 模块;
    })
    
  • 定义一个有依赖的模块

    define(['module1', 'module2'], function(m1, m2) {
    	return 模块;
    })
    

引入使用模块:

require(['module1', 'module2'], function(m1, m2) {
	// 使用m1, m2
})

举例1(引入require.js 单引用,引用文件名称)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <script src="require.js"></script>
    <script src='main.js'></script>
</head>

<body>
    <!-- 
        moduleA.js: 公共的可复用的内容
        main.js: JS程序入口
        index.html: 引入 main.js 内容
     -->
</body>

</html>

moduleA.js

define(function () {
    return {
        fooA1: function () {
            console.log("moduleA, function `fooA1`, Hello, world!");
        }
    };
});

main.js

require(['./moduleA.js'], function (mA) {
    mA.fooA1(); // moduleA, function `fooA1`, Hello, world!
})

举例2(引入require.js 单引用,使用配置文件别名)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <script src="require.js"></script>
    <script src='main.js'></script>
</head>

<body>
    <!-- 
        moduleA.js: 公共的可复用的内容
        main.js: JS程序入口
        index.html: 引入 main.js 内容
     -->
</body>

</html>

moduleA.js

define('ModuleAId', [], function () {
    return {
        fooA1: function () {
            console.log("moduleA, function `fooA1`, Hello, world!");
        }
    };
});

main.js

require.config({
    // 映射一些快捷路径,相当于别名
    paths: {
        'AA': "./ModuleA"
    }
})

require(['AA'], function (mA) {
    mA.fooA1();
});

举例3(module_id的使用,同一个作用域中)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <script src="require.js"></script>
    <script src='main.js'></script>
</head>

<body>
    <!-- 
        moduleA.js: 公共的可复用的内容
        main.js: JS程序入口
        index.html: 引入 main.js 内容
     -->
</body>

</html>

main.js

define('ModuleId1', [], function () {
    return {
        fooB1: function () {
            console.log("moduleB, function `fooB1`, Hello, world!");
        }
    };
});

require(['ModuleId1'], function (mB) {
    mB.fooB1(); // moduleB, function `fooB1`, Hello, world!
})

举例4(引入require.js 多引用)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <script src="require.js"></script>
    <script src='main.js'></script>
</head>

<body>
    <!-- 
        moduleA.js: 公共的可复用的内容
        main.js: JS程序入口
        index.html: 引入 main.js 内容
     -->
</body>

</html>

moduleA.js

define(function () {
    return {
        fooA1: function () {
            console.log("moduleA, function `fooA1`, Hello, world!");
        }
    };
});

moduleB.js

define(function () {
    return {
        fooB1: function () {
            console.log("moduleB, function `fooB1`, Hello, world!");
        }
    };
});

main.js

require(['./moduleA', './moduleB'], function (mA, mB) {
    mA.fooA1(); // moduleA, function `fooA1`, Hello, world!
    mB.fooB1(); // moduleB, function `fooB1`, Hello, world!
})

举例5(引入require.js 复合引用)

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*" />
    <title>My test page</title>

    <script src="require.js"></script>
    <script src='main.js'></script>
</head>

<body>
    <!-- 
        moduleA.js: 公共的可复用的内容
        main.js: JS程序入口
        index.html: 引入 main.js 内容
     -->
</body>

</html>

moduleA.js

define(function () {
    return {
        fooA1: function () {
            console.log("moduleA, function `fooA1`, Hello, world!");
        }
    };
});

moduleB.js

define(['./public'], function (p) {
    return {
        fooB1: function () {
            console.log("moduleB, function `fooB1`, Hello, world!");
        },
        fooB2: function () {
            p.fooPublicFun1();
        }
    };
});

public.js

define(function () {
    return {
        fooPublicFun1: function () {
            console.log("public, function `fooPublicFun1`, Hello, world!");
        }
    };
});

main.js

require(['./moduleA', './moduleB'], function (mA, mB) {
    mA.fooA1(); // moduleA, function `fooA1`, Hello, world!
    mB.fooB1(); // moduleB, function `fooB1`, Hello, world!
    mB.fooB2(); // public, function `fooPublicFun1`, Hello, world!
})

8.4 CMDJS的使用

在 Sea.js 中,所有 JavaScript 模块都遵循 CMD(Common Module Definition) 模块定义规范。该规范明确了模块的基本书写格式和基本交互规则。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

9 JSLint/JSHint/ESLint

posted on 2020-03-11 16:54  zyjhandsome  阅读(496)  评论(0)    收藏  举报