全篇来自廖雪峰老师的教程,本篇近作为笔记使用。

1、== 和===的区别

false == 0;    // true
false === 0;  // false

奇怪吧?因为:这属于 javascript 的一个缺陷

例外情况:
(1) NaN

NaN === Nan;  // false

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

isNaN(NAN);  // true

2、null 和 undefined**

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

3、变量

(1)命名
变量名是大小写英文、数字、$ 和_的组合,且不能用数字开头
注意:变量名也可以用中文,但是,请不要给自己找麻烦。
(2)变量定义
Javascript 定义变量:

var a = 123;  // a的值是整数123
a = 'ABC';  // a变为字符串

python 也是动态语言:

a = 123
a = "ABC"

Java 是静态语言,定义变量时必须指定变量类型:

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

4、数组

有一种数组长度说改就改:

// 直接给Array的length赋一个新的值会导致Array大小的变化:
var arr = [1, 2, 3];
arr.length; // 3
arr.length = 6;
arr; // arr变为[1, 2, 3, undefined, undefined, undefined]
arr.length = 2;
arr; // arr变为[1, 2]

// 如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化:
var arr = [1, 2, 3];
arr[5] = 'x';
arr; // arr变为[1, 2, 3, undefined, undefined, 'x']

数组常用函数:
(1)索引:通过 indexOf() 来搜索一个指定元素的位置

var arr = [10, 20, '30', 'xyz'];
arr.indexOf(10);  // 元素10的索引为0

(2)截取:slice() 可以截取 Array 的部分元素

var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
arr.slice(0, 3); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C']
arr.slice(3); // 从索引3开始到结束: ['D', 'E', 'F', 'G']

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

var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
var aCopy = arr.slice();

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

var arr = [1, 2];
arr.push('A', 'B'); // 返回Array新的长度: 4
arr; // [1, 2, 'A', 'B']
arr.pop(); // pop()返回'B'
arr.pop(); // 空数组继续pop不会报错,而是返回undefined

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

var arr = [1, 2];
arr.unshift('A', 'B'); // 返回Array新的长度: 4
arr; // ['A', 'B', 1, 2]
arr.shift(); // 'A'
arr; // ['B', 1, 2]
arr.shift(); // 空数组继续shift不会报错,而是返回undefined

(6)排序: sort() 可以对当前 Array 进行排序,它会直接修改当前 Array 的元素位置,直接调用时,按照默认顺序排序:

var arr = ['B', 'C', 'A'];
arr.sort();
arr; // ['A', 'B', 'C']

(7)反转: reverse() 把整个 Array 的元素给掉个个,也就是反转

var arr = ['one', 'two', 'three'];
arr.reverse(); 
arr; // ['three', 'two', 'one']

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

var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']

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

var arr = ['A', 'B', 'C'];
var added = arr.concat([1, 2, 3]);
added; // ['A', 'B', 'C', 1, 2, 3]
arr; // ['A', 'B', 'C']

请注意,concat() 方法并没有修改当前 Array,而是返回了一个新的 Array。

(10)join:join() 方法是一个非常实用的方法,它把当前 Array 的每个元素都用指定的字符串连接起来,然后返回连接后的字符串

var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'

(11)多维数组:
如果数组的某个元素又是一个 Array,则可以形成多维数组,例如:

var arr = [[1, 2, 3], [400, 500, 600], '-'];

5、对象

要判断一个属性是否是对象自身拥有的,而不是继承得到的,可以用 hasOwnProperty() 方法。不要用 in,继承得到的属性也会返回 true;

var xiaoming = {
    name: '小明'
};
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

6、条件判断

JavaScript 把 null、undefined、0、NaN 和空字符串' '视为 false,其他值一概视为 true.

7、iterable

for...in 循环问题:遍历的实际上是对象的属性名称
for...of,遍历的是对象的元素

更好的遍历方式:

// Array
var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
    // element: 指向当前元素的值
    // index: 指向当前索引
    // array: 指向Array对象本身
    console.log(element + ', index = ' + index);
});


// Set:Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});

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

// 如果只对某些参数感兴趣,可以忽略其他参数
var a = ['A', 'B', 'C'];
a.forEach(function (element) {
    console.log(element);
});

8、函数

(1)函数多传参或少传参问题

由于 JavaScript 允许传入任意个参数而不影响调用:

为避免收到 undefined,可以对参数进行检查:

function abs(x) {
    if (typeof x !== 'number') {
        throw 'Not a number';
    }
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

(2)arguments

JavaScript 还有一个免费赠送的关键字 arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments 类似 Array 但它不是一个 Array:

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

/*
x = 10
arg 0 = 10
arg 1 = 20
arg 2 = 30
*/

arguments 最常用于判断传入参数的个数

(3)rest 参数

如果使用 arguments 获取除已定义参数之外的参数,比较麻烦。所以,ES6 标准引入了 rest 函数,用于获取多余的传参。

function fooo (a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log('rest = ' + rest);
}
// 多传参:rest会获取多余参数
fooo(1, 2, 3, 4, 5)
/*
a = 1
b = 2
rest = 3,4,5
 */

// 少传参:rest参数会接受一个空数组
fooo(1)
/*
a = 1
b = undefined
[]
*/

(4)return 语句坑

由于 javascript 在行末自动添加分号,所以 return 语句需要写在同一行,或{...}中

// 正常的单行return
function foooo() {
    return { name: 'foo' };
}
foooo(); // { name: 'foo' }

// 多行return的坑
function fooooo() {
    return
    { name: 'foo' };
}
fooooo(); // undefined

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

(5)变量作用域与解构赋值

A. var申明的变量是有作用域的,作用域为整个函数体;
B. 变量提升

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

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}
foo();  // Hello, undefined
/*
因为JavaScript引擎自动提升了变量y的声明,所以函数不会报错;
但是不会提升变量y的赋值,所以变量y的值为undefined
*/

常见写法:用一个 var 在函数开头 申明所有变量

function foo() {
    var
        x = 1, // x初始化为1
        y = x + 1, // y初始化为2
        z, i; // z和i为undefined
    // 其他语句:
    for (i=0; i<100; i++) {
        ...
    }
}
C. 全局作用域

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

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

你可能猜到了,由于函数定义有两种方式,以变量方式 var foo = function () {}定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到 window 对象;
我们每次直接调用的 alert() 函数其实也是 window 的一个变量:

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

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

说明,JavaScript 只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续网上查找,最后如果在全局作用域中也没找到,则报 ReferennceError 错误。

D. 名字空间

全局变量会绑定到 window 上,不同的 JavaScript 文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

// 唯一的全局变量MYAPP:
var MYAPP = {};

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

// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间 MYAPP 中,会大大减少全局变量冲突的可能。jQuery、YUI、underscore 也这么做。

E. 局部作用域

var 定义的变量作用域:函数内部
let 定义的变量作用域:块

// var定义变量作用域在函数内
function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

// let定义变量可以达到块级作用域
function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

F. 常量

(ES6)const 申明常量。const 和 let 都具有块级作用域。

const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14
G. 解构赋值

ES6 引入:解构赋值,可以同时对一组变量进行赋值。

// 传统写法
var array = ['hello', 'Javascript', 'ES6']
var x = array[0];
var y = array[1];
var z = array[2];

// 如果浏览器支持解构赋值就不会报错:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];

注意,对数组元素进行解构赋值时,多个变量要用 [...] 括起来。
如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致:

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

解构赋值还可以忽略某些元素:

let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'

使用解构赋值,从对象中取出若干属性,便于快速获取对象的指定属性。

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

对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined

使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为 undefined,这和引用一个不存在的属性获得 undefined 是一致的。如果要使用的变量名和属性名不一致,可以用下面的语法获取:

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

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

解构赋值还可以使用默认值,这样就避免了不存在的属性返回 undefined 的问题:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
};

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

var {name, single=true} = person;
name; // '小明'
single; // true

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

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

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

({x, y} = { name: '小明', x: 100, y: 200});
H. 使用场景

4、方法

(1)函数内部的 this 到底指向谁?

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

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge()
}
xiaoming.age();  // 30,正常结果
getAge();  // NaN

JavaScript 函数中如果调用了 this,那么 this 到底指向谁?

apply 与 call

指定函数的 this 指向哪个对象,可以使用 apply 和 call:

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

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge()
}
xiaoming.age(); // 30
getAge().apply(xiaoming, [函数参数])
getAge().call(xiaoming, 函数参数)
// 对于普通函数调用,通常把this帮定为null
Math.max.apply(null, [1, 3, 5])
Math.max.call(null, 1, 3, 5)

装饰器

javaScript 的所有对象都是动态的,即使是内置的函数,也可以重新指向新的函数。
比如统计调用了多少次 parceInt(),可以如下:

'use strict';

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

parseInt = function () {  // 动态改变parseInt函数的行为
    count += 1;
    return oldParseInt.apply(null, arguments);  // 调用原函数
}

parseInt('10');
parseInt('20');
parseInt('30');
console.log(count)

高阶函数

接收函数作为参数的函数,称之为高阶函数。
一个最简单的高阶函数:

function add(x, y, f) {
    return f(x) + f(y);
}

(1)map

由于 map() 方法定义在 JavaScript 的 Array 中,我们调用 Array 的 map() 方法,传入我们自己的函数,就得到了一个新的 Array 作为结果:
map() 只接受一个入参的函数;

function pow(x) {
    return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

注意:map() 传入的参数是 pow,即函数对象本身。
map() 作为高阶函数,实际上把运算规则抽象了。

(2)reduce

Array 的 reduce() 把一个函数作用在这个 Array 的 [x1, x2, x3 ...] 上,这个函数必须接收两个参数,reduce() 把结果继续和系列的下一个元素做累积计算,其效果就睡:

[x1, x2, x3, x4].reduce(f) =  f(f(f(x1, x2), x3), x4)

(3)filter

filter() 接收一个函数,根据返回值是 true 还是 false 决定保留还是丢弃该元素。filter 接收的回调函数可以接收多个参数;
比如,去除 Array 中的重复元素:

var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function ( element, index, self ) {
    return self.indexOf(element) === index;
})

(4)sort

惊掉大牙的 sort() 结果

// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

原因:因为 Array 的 sort() 方法默认把所有元素先转换为 String 再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的 ASCII 码小。

sort() 是高阶函数,可以接收一个比较函数来实现自定义的排序

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    let a = 0;
    if (x < y) {
        a = -1;
    }
    if (x > y) {
        a = 1;
    }
    return a;
});
console.log(arr); // [1, 2, 10, 20]

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

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

every

every() 方法可以判断数组的所有元素是否满足测试条件。
例如:判断一个包含若干字符串的数组,判断所有字符串睡佛欧满足指定的测试条件:

var arr = ['Apple', 'pear', 'orange']
console.log(arr.every(function (s) {
    return s.length > 0;
}))  // true 因为每个元素都满足s.length>0

console.log(arr.every(function (s) {
    return s.toLowerCase() === s;
}))  // false 因为不是每个元素都全部是小写

find

find() 方法用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回 undefined;

var arr = ['Apple', 'pear', 'orange']
console.log(arr.find(function (s) {
    return s.toLowerCase() === s;
}));  // 'pear', 因为pear全部是小写

console.log(arr.find(function (s) {
    return s.toUpperCase() === s;
}));  // undefined, 因为没有全部是大写的元素

findIndex

findIndex() 和 find() 类似,也是查找符合条件的第一个元素,不同之处在于 findIndex() 会返回这个元素的索引,如果没有找到,返回-1;

forEach

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

闭包

高阶函数除了可以接收函数作为参数外,还可以把函数作为结果值返回。

箭头函数

ES6 标准新增了一种新的函数:Arrow Function(箭头函数)
箭头函数相当于一个匿名函数:

x => x * x
//相当于
function (x) {
  return x * x;
}

generator

gennerator(生成器)是 ES6 标准引入的新的数据类型。一个 generator 看上去像一个函数,但可以往返多次。
generator 的函数,在每次调用 next() 的时候执行,遇到 yield 语句返回,再次执行时从上次返回的 yield 语句处继续执行。

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

调用 generator 对象有两种方法:

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
var fib_obj = fib(10);
for (var x of fib_obj) {
    console.log(x);  // 依次输出0, 1, 1, 2, 3, ...
}

对象

标准对象

在 JavaScript 的世界里,一切都是对象。typeof 获取对象的类型。

typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'

包装对象

注意: 闲的蛋疼也不要使用包装对象,因为包装对象有神奇的 “催眠魔力 “。

var n = Number('123'); // 123,相当于parseInt()或parseFloat()
typeof n; // 'number'

var b = Boolean('true'); // true
typeof b; // 'boolean'

var b2 = Boolean('false'); // true! 'false'字符串转换结果为true!因为它是非空字符串!
var b3 = Boolean(''); // false

var s = String(123.45); // '123.45'
typeof s; // 'string'

总结一下,以下规则需要遵守:

注意:

123.toString(); // SyntaxError
// number转string要特殊处理以下
123..toString(); // '123', 注意是两个点!
(123).toString(); // '123'

Date

常见用法

要获取系统当前时间,用:

var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
now.getDate(); // 24, 表示24号
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳

注意,当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设定为任何值。

// 如果要创建一个指定日期和时间的Date对象,可以用:
var d = new Date(2015, 5, 19, 20, 15, 30, 123);
d; // Fri Jun 19 2015 20:15:30 GMT+0800 (CST)

JavaScript 神坑:JavaScript 的月份范围用整数表示是 0~11,0 表示一月,1 表示二月……。此处应该是设计者的 BUG。

// 标准时间转换为时间戳
var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875

// 时间戳转换为标准时间
var d = new Date(1435146562875);
d; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
d.getMonth(); // 5

时区

Date 对象表示的时间总是按浏览器所在时区显示的,不过我们既可以显示本地时间,也可以显示调整后的 UTC 时间:

var d = new Date(1435146562875);
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地时间(北京时区+8:00),显示的字符串与操作系统设定的格式有关
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT',UTC时间,与本地时间相差8小时

时间戳:时间戳是一个自增的整数,它表示从1970年1月1日零时整的 GMT 时区开始的那一刻,到现在的毫秒数。假设浏览器所在电脑的时间是准确的,那么世界上无论哪个时区的电脑,它们此刻产生的时间戳数字都是一样的,所以,时间戳可以精确地表示一个时刻,并且与时区无关。

RegExp 正则表达式

在正则表达式中,如果直接给出字符,就是精确匹配。匹配规则如下:

正则基础

进阶

要做更精确地匹配,可以用 [] 表示范围,比如:
(1)[0-9a-zA-Z_] 可以匹配一个数字、字母或者下划线;
(2)[0-9a-zA-Z_]+ 可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','js2015'等等;
(3)[a-zA-Z_\$][0-9a-zA-Z_\$]* 可以匹配由字母或下划线、$ 开头,后接任意个由一个数字、字母或者下划线、$ 组成的字符串,也就是 JavaScript 允许的变量名;
(4)[a-zA-Z_\$][0-9a-zA-Z_\$]{0, 19}更精确地限制了变量的长度是 1-20 个字符(前面 1 个字符 + 后面最多 19 个字符)。
(5)A|B 可以匹配 A 或 B,所以 (J|j) ava(S|s) cript 可以匹配'JavaScript'、'Javascript'、'javaScript'或者'javascript'。
(6)表示行的开头,\d 表示必须以数字开头。
(7)$ 表示行的结束,\d$ 表示必须以数字结束。

你可能注意到了,js 也可以匹配'jsp',但是加上js$ 就变成了整行匹配,就只能匹配'js'了。

正则的使用

JavaScript 有两种方式创建正则表达式:

var re1 = /ABC\-001/;
var re2 = new RegExp('ABC\\-001');

re1; // /ABC\-001/
re2; // /ABC\-001/

// 判断正则是否匹配
var re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false

切分字符串

// 用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:
'a b   c'.split(' '); // ['a', 'b', '', '', 'c']
// 嗯,无法识别连续的空格,用正则表达式试试:
'a b   c'.split(/\s+/); // ['a', 'b', 'c']
// 无论多少个空格都可以正常分割。加入,试试:
'a,b, c  d'.split(/[\s\,]+/); // ['a', 'b', 'c', 'd']
// 再加入;试试:
'a,b;; c  d'.split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']
// 如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。

分组

()表示的就是要提取的分组(Group)。

// ^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
var re = /^(\d{3})-(\d{3,8})$/;
re.exec('010-12345'); // ['010-12345', '010', '12345']
re.exec('010 12345'); // null

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

贪婪匹配

正则匹配默认是贪婪匹配,也就是匹配尽可能多的自负。

// 需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:
var re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']
// 由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。
// 必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:
var re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']

全局搜索

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

var r1 = /test/g;
// 等价于:
var r2 = new RegExp('test', 'g');

全局匹配可以多次执行 exec() 方法来搜索一个匹配的字符串。当我们指定 g 标志后,每次运行 exec(),正则表达式本身会更新 lastIndex 属性,表示上次匹配到的最后索引:

var s = 'JavaScript, VBScript, JScript and ECMAScript';
var re=/[a-zA-Z]+Script/g;

// 使用全局匹配:
re.exec(s); // ['JavaScript']
re.lastIndex; // 10

re.exec(s); // ['VBScript']
re.lastIndex; // 20

re.exec(s); // ['JScript']
re.lastIndex; // 29

re.exec(s); // ['ECMAScript']
re.lastIndex; // 44

re.exec(s); // null,直到结束仍没有匹配到

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

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

JSON

发明者:道格拉斯·克罗克福特(Douglas Crockford)
诞生时间:2002 年
JSON 中数据结构:

JavaScript 中可以直接使用 JSON,因为 javascript 内置了 JSON。

序列化

将 JavaScript 对象序列化成 JSON 格式的字符串

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

var s = JSON.stringify(xiaoming);
console.log(s);

// 要输出的好看些,按照缩进输出:
var s = JSON.stringify(xiaoming, null, '  ');   
console.log(s);

// 第二个参数表示输出指定的属性
var s = JSON.stringify(xiaoming, ['name', 'height'], '  ');

// 还可以传入一个函数,这样对象的每个键值对都会被函数先处理
function convert(key, value) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    }
    return value;
}

JSON.stringify(xiaoming, convert, '  ');

// 如果还想要精确控制如何序列化对象,可以给对象定义一个toJSON()的方法,直接返回JSON应该序列化的数据:
var xiaoming = {
    name: '小明',
    age: 14,
    gender: true,
    height: 1.65,
    grade: null,
    'middle-school': '\"W3C\" Middle School',
    skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
    toJSON: function () {
        return { // 只输出name和age,并且改变了key:
            'Name': this.name,
            'Age': this.age
        };
    }
};

JSON.stringify(xiaoming); // '{"Name":"小明","Age":14}'

反序列化

用 JSON.parse() 将 JSON 格式的字符串转为 JavaScript 对象;

let js_obj = JSON.parse('{"Name":"小明","Age":14}')
console.log(js_obj);

JSON.parse() 还可以接收一个函数,用来转换解析出的属性:

let js_obj1 = JSON.parse('{"Name":"小明","Age":14}', function (key, value) {
    if (key === 'Name') {
        return value + '小伙伴';
    }
    return value;
})

console.log(JSON.stringify(js_obj1))

/* 输出:
{"Name":"小明小伙伴","Age":14}
*/

面向对象编程

JavaScript 的所有数据都可以看成对象。

JavaScript 的面向对象编程和大多数其他语言如 Java、C# 的面向对象编程都不太一样。
Java 的面向对象有两个基本概念:

JavaScript 不区分类和实例的概念,而是通过原型 (prototype) 来实现面向对象编程。

var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent (name) {
    //基于Student原型创建一个新对象
    var s = Object.create(Student);
    // 初始化新对象
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run();  // 小明 is running...
xiaoming.__proto__ === Student;  // true

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

创建对象

JavaScript 对每个创建的对象都会设置一个原型,指向它的原型对象。
当我们用 obj.xxx 访问一个对象的属性时,JavaScript 引擎现在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到 Object.prototype 对象,最后,如果还没有找到,就只能返回 undefiined。

例如:创建一个 Array 对象:

var arr = [1, 2, 3];

其原型链是:

arr  ----->  Array.prototype  ----->  Object.prototype  -----> null

Array.prototype 定义了 indexOf()、shift() 等方法,因此所有的 Array 对象可以直接调用这些方法。
注意:如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞的太长。

构造函数

除了直接用{ ... }创建一个对象外,还可以用构造函数的方法来创建对象。

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}

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

注意:如果不写 new,这就是个普通函数,它返回 undefined。如果写了 new,它就变成了一个构造函数,它绑定的 this 指向新创建的对象,并默认返回 this。

忘记写 new 怎么办?

千万不要忘记写 new。在 strict 模式下,this.name = name 将报错,因为 this 绑定为 undefined,在非 strict 模式下,this.name = name 不报错,因为 this 绑定为 window,于是无意间创建了全局变量 name,并且返回 undefined,这个结果更糟糕。

所以,调用构造函数千万不要忘记写 new。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如 jslint 将可以帮你检测到漏写的 new。

最后,可以编写一个 createStudent() 函数,在内部封装所有的 new 操作:


function Student(props) {
    this.name = props.name || '匿名'; // 默认值为'匿名'
    this.grade = props.grade || 1; // 默认值为1
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

function createStudent(props) {
    return new Student(props || {})
}

var xiaoming = createStudent({name: '小明'});
console.log(xiaoming.grade);

原型继承

JavaScript 的原型继承实现方式就是:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

// PrimaryStudent构造函数
function PrimaryStudent (props) {
    // 调用Student构造函数,绑定this变量
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 原型继承函数
function inherits (Child, Parent) {
    // 空函数F
    var F = function () {};
    // 把F的原型指向Parent.prototype:
    F.prototype = Parent.prototype;
    // 把Child的原型指向一个新的F对象,F对象的原型正好指向Parent.prototype:
    Child.prototype = new F();
    // 把Child原型的构造函数修复为Child:
    Child.prototype.constructor = Child;
}

// 实现原型继承链
inherits(PrimaryStudent, Student);

class 继承

原型继承理解困难,实现需要编写大量代码,有没有更简单的办法?有
新的关键字 class 从 ES6 开始正式被引入到 JavaScript 中。class 的目的就是让定义类更简单。

(1)class 定义原型

class Student {
    constructor (name) {
        this.name = name;
    }

    hello() {
        alert('Hello ' + this.name + '!');
    }
}

var xiaoming = new Student('小明');
xiaoming.hello();

(2)class 继承

// extends则表示原型链对象来自Student
class PrimaryStudent extends Student{
    // 构造函数
    constructor (name, grade) {
        //通过super(name)来调用父类的构造函数
        super(name);
        this.grade = grade;
    }

    myGrade() {
        return this.grade;
    }
}

浏览器

浏览器对象

(1)window

window 对象不但充当全局作用域,而且表示浏览器窗口。
内部宽高:指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。

(2)navigator

navigator 对象表示浏览器的信息,最常用的属性包括:

请注意,navigator 的信息可以很容易地被用户修改,所以 JavaScript 读取的值不一定是正确的

(3)screen

screen 对象表示屏幕的信息,常用的属性有:

(4)location

location 对象表示当前页面的 URL 信息。

(5)document

document 对象表示当前页面。由于 HTML 在浏览器中以 DOM 形式表示为树形结构,document 对象就是整个 DOM 树的根节点。

document 的 title 属性是从 HTML 文档中的

xxx读取的,但是可以动态改变:
document.title = '努力学习JavaScript!';

要查找 DOM 树的某个节点,需要从 document 对象开始查找。最常用的查找是根据 ID 和 Tag Name。

(6)history

任何时候,都不应该使用 history 这个对象了。

操作 DOM

由于 HTML 文档被浏览器解析后就是一棵 DOM 树,要改变 HTML 的结构,就需要通过 JavaScript 来操作 DOM。
始终记住 DOM 是一个树形结构。操作一个 DOM 节点实际上就是这么几个操作:

// 先定位 ID 为'test-table'的节点,再返回其内部所有 tr 节点:
var trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位 ID 为'test-div'的节点,再返回其内部所有 class 包含 red 的节点:
var reds = document.getElementById('test-div').getElementsByClassName('red');

// 获取节点 test 下的所有直属子节点:
var cs = test.children;

// 获取节点 test 下第一个、最后一个子节点:
var first = test.firstElementChild;
var last = test.lastElementChild;

// 通过 querySelector 获取 ID 为 q1 的节点:
var q1 = document.querySelector('#q1');

// 通过 querySelectorAll 获取 q1 节点内的符合条件的所有节点:
var ps = q1.querySelectorAll('div.highlighted > p');


#### 更新DOM
更新方式由两种:
(1)修改innerHTML属性:这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树:
```javascript
// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的内部结构已修改

(2)修改 innerText 或 textContent 属性,这样可以自动对字符串进行 HTML 编码,保证无法设置任何 HTML 标签:

// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:

(3)修改 css 属性

// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

插入 DOM

(1)如果这个 DOM 节点是空的,例如,

,那么,直接使用 innerHTML = 'child'就可以修改 DOM 节点的内容,相当于 “插入” 了新的 DOM 节点。

(2)如果这个 DOM 节点不是空的,那就不能这么做,因为 innerHTML 会直接替换掉原来的所有子节点。
有两个办法可以插入新的节点:


↙↙↙阅读原文可查看相关链接,并与作者交流