JavaScript的学习笔记及细节问题总结

写在前面

本文主要参考了廖雪峰的官方网站

JavaScript有很多设计缺陷,因此JavaScript会有些令人感到匪夷所思的地方,引用廖雪峰老师的话:“不要问为什么,这就是JavaScript代码的乐趣!”。对JavaScript最大的总结是:JavaScript规定可以但是不常用或者不合逻辑的语法就不要使用,否则就是自找麻烦。然而要想精通JavaScript,又有必要知道这些不合逻辑的奇妙语法。

一、句尾的;

JavaScript并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;

要养成好习惯,在语句的结尾手动加上;因为让JavaScript引擎自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致。
在JavaScript中,不加分号报错一共只有6种情况:语句以+(加号), -(减号),[(左方括号),((左括号),/(除号或者正则表达式的开始符),`(模板字符串)这6种字符之一开头。除此之外return后换行会导致返回值不符合预期,但不会报错。

return后换行:

function foo() {
    return
        { name: 'foo' };
}

foo(); // undefined

// 由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了:
function foo() {
    return;  //自动添加了分号,相当于return undefined;
        { name: 'foo' };  //这行语句已经没法执行到了
}

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

对于报错的6种情况,举几个例子:对对象进行解构赋值(解构赋值详见下一章)时,例如:({a, b} = {a: 1, b: 2}),若此语句的前一条语句结尾没有;,则(...)可能会被当成上一行的函数执行:

var a, b, c;
console.log(c); // undefined
({a, b} = {a: 1, b: 2}); // {a: 1, b: 2}

// 若不加分号
var a, b, c
console.log(c)
({a, b} = {a: 1, b: 2}) // Uncaught TypeError: console.log(...) is not a function

自调用函数的前一条语句若没有;也会产生同样的错误。在使用自调用函数时可以在自调用函数前也加一个;可以完全防止这种问题。例如:

var s = {};
s.a = function() {
    console.log(1);
} // 缺少分号
+ function() { // 自调用函数
    s.a();
}(); 
// Uncaught TypeError: s.a is not a function

var s = {};
s.a = function() {
    console.log(1); 
} // 缺少分号
(function() { // 自调用函数
    s.a();
}());
// Uncaught TypeError: s.a is not a function

// JavaScript文件合并时,若其他人写的代码结尾没有分号,文件第一行的自调用函数也可能会出现意外的运行结果
// 自调用函数前写一个分号,可以完全防止以上问题
;(function() {
  // function body...
});

二、变量

  1. 变量名可以使用中文,但这只会引发不必要的麻烦。
  2. JavaScript可以使用varletconst来申明变量:
  • var可声明全局或函数级别作用域的变量;
  • let可以声明块级别作用域的变量,ES6标准引入的新关键字;
  • const声明变量时,必须给变量赋初值,且该值在整个代码的运行过程中不能被修改,const变量也不能重复多次声明。const同样是ES6标准引入的新关键字。
  1. varlet声明的变量没有初始化时,值为undefined,而const声明的变量没有初始化会报错:Uncaught SyntaxError: Missing initializer in const declaration
  2. var声明支持变量提升,而constlet声明不支持变量提升。
  3. 声明提升
    声明提升是指:在JavaScript程序解释执行前,即预编译阶段,JavaScript引擎会扫描当前运行的整个代码块,把变量以及函数的声明提升到当前代码块的开头。
    特别说明:此处的代码块特指当前函数体或者当前整个脚本文件,因为在其他代码块中要么不会声明提升,要么会出现特殊情况,详见后文分析。

注意:函数提升优先于变量提升,即在声明提升后,函数声明总在变量声明之前。

  • 变量提升:
function foo() {
    var x = 'Hello, ' + y;
    console.log(x); // Hello, undefined
    var y = 'Bob';
}

// JavaScript引擎看到的代码相当于:
function foo() {
    var x; // 提升变量x的声明
    var y; // 提升变量y的声明,此时y为undefined
    x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}
  • 函数提升:
// 普通函数声明的提升:
f('tom'); // tom
function f(name){
    console.log(name);
}

// 等价于
function f(name){ // 函数的声明会被提升
    console.log(name);
}
f('tom');

// 匿名函数赋值给变量时的提升:
f('tom'); // Uncaught TypeError: f is not a function
var f= function(name){ // 这种方式定义的函数只会提升变量f的声明
    console.log(name);
}

// 等价于:
var f;
f('tom'); // 此时f是undefined
f= function(name){
    console.log(name);
}

// 既有函数声明又有变量声明时:
var f= function(){
    console.log(2);
}
function f(){
    console.log(1);
}
f(); // 2

// 等价于:
function f(){ // 函数声明提升,优先级高于变量提升
    console.log(1);
}
var f; // 变量声明提升
f= function(){
    console.log(2);
}
f(); // 2

if或类似代码块中的变量,如果是var声明则是全局变量,如果是其他方式则不会变量提升,会造成暂时性死区,不存在变量提升的问题。但是如果有函数声明,则会造成非常诡异的情况:

// 1. if里的函数声明首先会定义一个全局同名变量a=undefined
console.log(window.a, a); // undefined undefined
if(true){
    // 2. if里的函数赋值会提升到块作用域顶部
    console.log(window.a, a); // undefined ƒ a(){console.log(2);}
    a = 1;
    console.log(window.a, a); // undefined 1
    a = function(){console.log(1);};
    console.log(window.a, a); // undefined ƒ (){console.log(1);}
    // 3. 执行到函数声明语句时,会把块作用域里的a赋值到全局同名变量a
    function a(){console.log(2);}
    console.log(window.a, a); // ƒ (){console.log(1);} ƒ (){console.log(1);}
}
a(); // 1
console.log(window.a, a); // ƒ (){console.log(1);} ƒ (){console.log(1);}

除此之外,如果在if或类似代码块中,如果同时出现一个变量的变量声明和函数声明,就会报错。

if(true){
    function a(){}
    var a;
}
// Uncaught SyntaxError: Identifier 'a' has already been declared
  1. var声明的变量,不支持块级作用域,letconst声明的变量支持块级作用域,例如:
if (true) {
  let num = 3;
  const msg = "How are you?";
}
alert(num); //num为块级变量,离开判断块后无效,所以报错:Uncaught ReferenceError 
alert(msg); //msg为块级变量,离开判断块后无效,所以报错:Uncaught ReferenceError
for (let i = 0; i < 9; i ++ ) {     
  var j = i;
}
alert(i);  //i为块级变量,离开循环块后无效,所以报错:Uncaught ReferenceError
alert(j);  //j为全局变量,离开循环块后仍有效,所以运行正常,输出:8
  1. 在同一个作用域中,var可以重复声明同一个变量,letconst不能重复声明同一个变量。
  2. letconst存在暂时性死区(temporal dead zone,简称 TDZ):如果区块中存在letconst申明,那么这个区块对这些声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。例如:
var tmp = 123;

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}
  • “暂时性死区”也意味着typeof不再是一个百分之百安全的操作。作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错:
typeof x; // 报错:ReferenceError
let x;
// 变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错
// 因此,`typeof`运行时就会抛出一个ReferenceError。

typeof undeclared_variable // 不报错 输出:'undefined'
  • 特殊“暂时性死区”:
// 使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。

function bar(x = y, y = 2) { // 参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“
  return [x, y];
}
bar(); // 报错

var x = x; // var声明不会造成暂时性死区,不报错
let x = x; // 变量x的声明语句还未执行就去取x的值,造成暂时性死区
// 报错:ReferenceError: x is not defined

特别注意:箭头函数递归时,无论函数是使用letvar还是const声明,都不会发生暂时性死区。

// 箭头函数递归不会发生暂时性死区
let f = n => n < 2 ? 1 : n * f(n - 1);
f(4); // 24

假设不使用JS语法糖,使用箭头函数实现递归,参考:在 JavaScript 中用匿名函数(箭头函数)写出递归的方法-GitHub

保持好习惯,变量一定要先声明再使用,将一个代码块中所有变量或函数的声明都放在块的开头,不要在任何if或类似代码块中声明函数,在代码块中声明变量时使用letconst而不要使用var,只有当定义全局变量时使用var

  1. JavaScript并不强制要求声明变量,但是如果一个变量没有通过声明就被使用,那么该变量就自动被声明为全局变量。这种行为可能带来严重的后果,可以使用严格模式(strict mode)来防范这种错误。

关于严格模式可参考:严格模式-MDN

  1. ES6中可以使用解构赋值,直接对多个变量同时赋值:
// 数组元素进行解构赋值时,多个变量要用[]括起来
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
console.log('x = ' + x + ', y = ' + y + ', z = ' + z);
// x = hello, y = JavaScript, z = ES6

// 嵌套的数组进行解构赋值时,嵌套层次和位置要保持一致
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

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

// 对象进行解构赋值时,则要用{}括起来
var {p, q} = {p: 42, q: true};
console.log(p); // 42
console.log(q); // true

// 使用自定义变量名解构赋值
var o = {p: 42, q: true};
var {p: foo, q: bar} = o; // 注意:此处的p, q并不是变量,foo, bar才是
console.log(foo); // 42
console.log(bar); // true 

// 多级解构赋值
var a = {
    b: {
        c: 42,
        d: true
    },
    e: 'hello'
};
var {b: {c, d}} = a; // 同上,此处的b也不是可用变量
console.log(c); // 42
console.log(d); // true 

// 带有默认值的解构赋值
var {a = 10, b = 5} = {a: 3};
console.log(a); // 3
console.log(b); // 5

// 解构赋值综合案例
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,passport:id, address: {city, zip}, single=true} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量:
passport; // Uncaught ReferenceError: passport is not defined
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量:
address; // Uncaught ReferenceError: address is not defined
// person对象没有single属性,默认赋值为true:
single; // true

注意:有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误,这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来:

// 声明并解构赋值,这样不会引起任何问题:
var {x, y} = { name: '小明', x: 100, y: 200};

// 但对已经声明过的变量直接解构赋值,会产生错误:
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =

//解决方法:
({x, y} = { name: '小明', x: 100, y: 200});

三、数据类型

1. Number
  • JavaScript不区分整数和浮点数,统一用Number表示。
  • NaNInfinity也是Number类型,NaN表示Not a Number,当无法计算结果时用NaN表示,Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity
  • null表示一个“空”的值,而undefined表示“未定义”。大多数情况下,都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。
2. 字符串
  • 字符串具有不变性,字符串内置的方法都不会改变字符串本身的内容,而是返回一个新的字符串。此外,如果对字符串的某个索引赋值,不会有任何错误,但是,也不会有任何效果:
var s = 'Test';
s[0] = 't';
alert(s); //s仍然为'Test'
  • 使用substring()方法传入2个参数如startend时,截取的子字符串包含索引为start的字符,但是不包含索引为end的字符:
var s = 'hello, world'
s.substring(0, 5); //从索引0开始到5(不包括5),返回'hello'
  • ES6标准新增了模板字符串,用反引号`表示:
// 普通字符串需要显示多行需要多个\n并连接字符串,非常麻烦
console.log('这是一个\n' +
'多行\n' +
'字符串\n');

// 使用模板字符串则简单很多
console.log(`这是一个
多行
字符串`);

// 普通字符串这样写则会报错
console.log('这是一个
多行
字符串');
// Uncaught SyntaxError: Invalid or unexpected token

// 模板字符串更常用的用法,使用${变量名}的形式来替换字符串中变量的值
var s = 'world';
console.log(`hello, ${s}!`); // hello, world!
3. 数组
  • JavaScript的数组可以包括任意数据类型。
  • 可以通过Array()函数创建数组,不过此方法并不常用:
new Array(1, 2, 3); //创建数组[1, 2, 3]
  • 直接给Arraylength赋一个新的值会导致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']

由上述两点可知,JavaScript的数组并不会发生数组越界的错误,但是尽量不要直接修改Array的大小,访问索引时要确保索引不会越界。

  • 数组的slice()方法类似字符串的substring()方法,截取子数组时并不包含结束索引。如果不给slice()传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个Array
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
var copy= arr.slice();
copy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G']
copy === arr; // false

[].slice.call(Object)可以把具有length属性的对象转成数组。[].slice.call()具有比Array.prototype.slice.call()更高的性能。

  • splice()方法是修改Array的“万能方法”,splice(start, count, ...list)会从索引start开始删除count个元素(包含start),然后再把参数list作为内容添加进数组,添加的位置从索引start处开始。当只传入第一个参数时,splice会将索引start后的所有元素全部删除(同样包含start)。此外可以通过将第二个参数count设为0来在索引start处只添加元素而不删除。无论splice()方法传入多少个参数,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']

数组的常用操作方法中slice()concat()join()方法不会改变原数组,而是返回一个新数组,而push()pop()unshift()shift()sort()reverse()splice()这些方法都会直接改变数组。

4. 对象
  • 如果属性名包含特殊字符,就必须用''括起来,访问这个属性也无法使用.操作符,必须用['属性名']来访问:
var xiaohong = {
    name: '小红',
    'middle-school': 'No.1 Middle School'
};
xiaohong['middle-school']; // 'No.1 Middle School'
xiaohong['name']; // '小红'
xiaohong.name; // '小红'
  • JavaScript对象的所有属性本质上都是字符串,但属性对应的值可以是任意数据类型。此外,如果访问的属性不存在并不会报错,而是返回undefined
  • 使用in判断一个对象存在某个属性时,这个属性不一定是该对象的,它可能是该对象继承得到的,要判断一个属性是否是对象自身拥有的,而不是继承得到的,需要用hasOwnProperty()方法。例如:
var xiaoming = {
    name: '小明'
};
'name' in xiaoming; // true
'grade' in xiaoming; // false
'toString' in xiaoming; // true
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false
5. MapSet
  • MapSet是ES6标准新增的数据类型。
  • Map和对象一样也是一组键值对的结构,区别在于Map的键可以是任意数据类型,而对象的属性只能是字符串,此外Map具有极快的查找速度,而且Map中不能存储相同的元素,因为多次对一个key放入value,后面的值会把前面的值冲掉。
  • SetMap类似,不能存储相同的值,但是Set只存储值而不存储键。

四、条件判断

不需要把对象转换为boolean再判断。

NaN与所有其他值都不相等,包括它自己,唯一能判断NaN的方法是通过isNaN()函数:

NaN === NaN; //false
isNaN(NaN); //true

注意相等运算符==,JavaScript在设计时,有两种比较相等的运算符:

  • 第一种是==比较,它会自动转换数据类型再比较,有些时候,会得到非常诡异的结果;
  • 第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。
0 == +0 == -0 == false == "" // 这些使用==比较全都相等,前三个互相之间使用===也相等,因为类型相同
undefined == null // true  使用===则是false

因此不要使用==比较,应使用===比较。

注意:JavaScript把nullundefined0NaN和空字符串''都视为false,其他值一概视为true
牢记:对象{}在布尔运算时视为true,因为真正的空对象是null

特别注意:空数组[]在布尔运算时同样视为true,但是当[]直接与布尔值进行比较时,[]视为false

if([]) {
	console.log("true");
} // true

[] == false; // true,因为Number([])===0,Number(false)===0

这是因为任意对象直接与布尔值进行比较时,两边会转成number类型再比较。因此对象直接与布尔值比较会出现下面这些奇妙的情况:

// 本段代码注释中的=理解成数学上的等号,代表表达式在浏览器控制台中的结果,不是赋值号
[] == ![]; // true,![]即布尔值false,等价于[] == false
1 == true; // true,Number(true)=1
2 == true; // false,Number(true)不等于2

// 绝大多数Object直接转换成number都是NaN
({}) == true; // false
({}) == false; // false,因为Number({})=NaN,所以既不等于true,也不等于false

// 对于数组,若length == 0,转换成number就是0
// 若length == 1,则直接对唯一的元素再进行Number()运算(undefined除外)(个人理解,Number()函数的源码不一定这样写的)
// 只要length >= 2,转换成number就是NaN
[1] == true; // true,Numebr([1])=Number(1)=1
[1, 1] == true; // false,Number([1, 1]) = NaN
// undefined是例外,不满足Number([undefined])=Number(undefined)
// Number([undefined])=0,而Number(undefined)=NaN
[undefined] == false; // true,Number([undefined])=0
[undefined, undefined] == false; // false,length=2所以Number([undefined, undefined])=NaN
[null] == false; // true,Number([null])=Number(null)=0
// 在浏览器控制台中[,]显示:[空] { length: 1 [[Prototype]]... },没有下标0的元素
// 而[undefined]显示是:[undefined] { 0: undefined length: 1 [[Prototype]]... },下标为0的元素存在,是undefined
// 然而在行为上这两者并无二异,输出下标为0的元素都是undefined
new Array(1) == false; // true,new Array(1)与[,]等价,空数组但length属性=1
new Array(2) == false; // false,new Array(2)与[,,]等价,空数组但length属性=2
[,] == false; // true,由于[,]与[undefined]具有相同的行为,所以Number([,])也等于0
[,,] == false; // false,[,,]则与[undefined, undefined]相同,所以Number([,,])也等于NaN
[[]] == false; // true,Number([[]])=Number([])=0
[[1]] == true; // true,Number([[1]])=Number([1])=Number(1)=1
[[], []] == false; // false,length=2所以Number([[], []])=NaN

为了避免不必要的麻烦,不要用对象与boolean类型直接进行比较。

由于计算机无法精确表示无限循环小数,而用二进制表示十进制小数常常会得出无限循环小数,因此判断浮点数相等不能直接使用===,需要计算它们之差的绝对值,看它是否小于某个很小的值:

1 / 3 === (1 - 2 / 3); //false
Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; //true

特别注意:在JavaScript中&&||运算符的返回值不仅限于truefalse,而是返回一个任意对象。
对于||

  • 若左侧是false,则返回右侧的对象;
  • 若左侧是true,则直接返回左侧的对象,右侧对象或表达式被短路;

对于&&

  • 若左侧是true,则返回右侧的对象;
  • 若左侧是false,则直接返回左侧的对象,右侧对象或表达式被短路;

五、循环

  • for ... in循环可以把一个对象的所有属性依次循环出来,由于数组是特殊的对象,因此对于数组,for ... in是循环出数组的索引,不论是普通对象还是数组,循环变量的类型总是String而不是Number
  • for ... of循环是ES6引入的新的语法,具有iterable类型的集合可以通过新的for ... of循环来遍历,ArrayMapSet都属于iterable类型。for ... of的循环变量是集合的元素本身而不是属性或索引。
  • iterable内置forEach方法,它接收一个回调函数function (value, key, iterable)作为参数,value指向当前元素的值,key指向当前索引,iterable指向Iterable对象本身。需要注意的是,Set类型并没有索引,因此回调函数中第二个参数依然指向当前元素的值。
var a = ['A', 'B', 'C'];
a.name = 'Hello'; // 手动给Array对象添加额外的属性

for (var x in a) {
    console.log(x); // '0', '1', '2', 'name'
    console.log(a[x]); // 'A', 'B', 'C', 'Hello'
}

for (var x of a) {
    console.log(x); // 'A', 'B', 'C'
}

a.forEach(function (element, index, array) { // 参数传入回调函数
    console.log(element + ', index = ' + index);
});

六、函数

1. 参数
  • JavaScript允许传入任意个参数而不影响调用。
  • 可以使用arguments对象来获得调用者传入的所有参数。arguments只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments对象类似Array但它不是一个Array,除了length属性和索引元素之外没有任何Array属性,但是它可以被转换为一个真正的Array。利用arguments,即使函数不定义任何参数,还是可以拿到参数的值。
  • ES6标准引入了rest参数,rest参数只能写在最后,前面用...标识,前面的参数依次传入,多余的参数以数组形式交给变量rest
function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []

...运算符还可以用于展开数组或对象:

var arr = [1, 2, ...[3, 4], 5]; // [1, 2, 3, 4, 5]
var obj1 = {
    a: 1,
    b: 2,
    ...{c: 3, d: 4},
    e: 5
}; // {a: 1, b: 2, c: 3, d: 4, e: 5}

// 可以通过...运算符简单的拷贝一个数组或对象
var obj2 = { ...obj1 }; // {a: 1, b: 2, c: 3, d: 4, e: 5}
obj1 === obj2; // false
2. 返回值
  • 如果没有return语句,则返回值为undefined
  • 构造函数默认返回this对象。
3. 全局作用域
  • JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性,因此,直接访问全局变量和访问window.全局变量名是完全一样的。顶层函数的定义也被视为一个全局变量,并绑定到window对象。
  • 不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。减少冲突的一个方法是把自己的所有全局变量和顶层函数全部绑定到一个全局变量中。例如:
// 唯一的全局变量MYAPP:
var MYAPP = {};

// 全局变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 顶层函数:
MYAPP.foo = function () {
    return 'foo';
};
4. 方法
  • 对象中绑定的函数称为这个对象的方法,全局函数实际上是全局对象window的方法。
  • 调用对象方法的方式为对象名.方法名(),若不写(),则返回的是函数定义。
  • 在方法内部,this始终指向当前对象,无法改变this的值。但是在函数中使用this时,this是指向全局对象window,在严格模式下函数的this是指向undefined。在方法内的函数需要使用this时,可以通过用一个that变量获得this。例如:
var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var that = this; // 在方法内部一开始就捕获this
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - that.birth; // 用that而不是this
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // 31
  • 可以使用函数自带的call()apply()方法实现装饰器,来动态改变函数的行为。这两个方法的第一个参数都是要接收一个对象,用于绑定this的指向,区别在于后面的参数:call()把参数按顺序传入,apply()把参数打包成Array再传入。例如:
// apply()和call()的区别
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

// 实现装饰器
var count = 0;
var oldParseInt = parseInt; // 保存原函数

window.parseInt = function () { // 给原函数增加记录已调用次数的功能
    count += 1;
    return oldParseInt.apply(null, arguments); // 调用原函数,此处只能用apply()
};
parseInt('10'); // 10
parseInt('20'); // 20
parseInt('30'); // 30
console.log('count = ' + count); // 3
5. 高阶函数
  • ArrayMap()方法传入一个函数作为参数,使Array中的每个元素按照该函数的映射关系生成一个新数组:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

arr.map(x => x * x); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
  • 要注意map()方法接收的回调函数可以有3个参数callbackFn(element, index, array),而当传入的回调函数拥有多个参数却只需要用到第一个参数时,可能会得到预料之外的结果:
var arr = ['1', '2', '3'];

arr.map(parseInt); // [1, NaN, NaN]
// parseInt(string, radix)具有2个参数,第二个参数是表示需要转换的进制
// 实际每次回调时执行的是:
// parseInt('1', 0)   1, 按十进制转换
// parseInt('2', 1)   NaN, 没有一进制
// parseInt('3', 2)   NaN, 按二进制转换, 不允许出现3

// 解决方法:
arr.map(Number) // [1, 2, 3]   Number(value)仅需要一个参数
  • Arrayreduce()方法同样传入一个函数作为参数,将Array的前2个元素通过该函数得到一个返回值,然后每次用上次获得的返回值与Array中的后一个元素作为函数参数作累计运算,得到一个最终的累计值作为reduce()方法的返回值:
// reduce()方法的效果相当于:
// [x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4);

var arr = [1, 3, 5, 7, 9];
arr.reduce((x, y) => x + y); // 25
arr.reduce((x, y) => x * y); // 945
arr.reduce((x, y) => (10 * x + y)); // 13579
  • filter()也是接收一个函数作为参数,但是该函数需要返回一个truefalsefilter()返回一个新数组,数组包含所有使回调函数返回true的元素。可以使用filter()方便的去除数组中的重复元素:
var arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
arr.filter((element, index, array) => array.indexOf(element) === index);
// indexOf()返回该元素在数组中第一次出现的位置,若不等于当前位置则表示这是个重复元素
  • sort()方法默认把所有元素先转换为String再排序,这会导致直接使用sort()对数字进行排序时产生不正确的结果。sort()方法可以接收一个回调函数作为参数来实现自定义排序,回调函数需要具有2个参数ab,若需要a排在b前面,则返回一个负数,否则返回一个正数,若ab相等,则返回0。例如:
var arr = [10, 20, 1, 2];
arr.sort(); // [1, 10, 2, 20]  默认转换成字符串再排序
arr.sort((a,b) => a-b); // [1, 2, 10, 20]  正序
arr.sort((a,b) => b-a); // [20, 10, 2, 1]  倒序

// 要注意sort()方法会直接操作当前数组,而不是返回新数组
arr; // [20, 10, 2, 1]
  • Array的其他常用高阶函数示例:
var arr = ['Apple', 'pear', 'orange'];

// every()用于判断数组的所有元素是否都满足测试条件
arr.every(s => s.length > 0); // true  因为每个元素都满足s.length>0
arr.every(s => s.toLowerCase() === s); // false  因为不是每个元素都全部是小写

// find()用于查找符合条件的第一个元素,如果找到了,返回这个元素,否则,返回undefined
arr.find(s => s.toLowerCase() === s); // 'pear'  因为pear是第一个全部是小写的元素
arr.find(s => s.toUpperCase() === s); // undefined  因为没有全部是大写的元素

// findIndex()和find()类似,也是查找符合条件的第一个元素
// 区别在于findIndex()会返回这个元素的索引,如果没有找到,返回-1
arr.findIndex(s => s.toLowerCase() === s); // 1  因为'pear'的索引是1
arr.findIndex(s => s.toUpperCase() === s); // -1  因为没有全部是大写的元素

// forEach()和map()类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组
// forEach()常用于遍历数组,因此,传入的函数不需要返回值
arr.forEach(console.log); // 依次打印每个元素
6. 闭包
  • 闭包通过函数返回函数,将相关参数和变量都保存在返回的函数中,实现了一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。直观的说就是形成一个不销毁的栈环境。例如:
var add = (function () { // 自调用函数,保证counter只初始化一次
    var counter = 0; // counter被这个匿名函数的作用域保护,并且只能使用 add 函数来修改
    return function () {return counter += 1;} // 返回的函数能访问上一层匿名函数的作用域中的counter变量
})();

add(); // 1
add(); // 2
add(); // 3
  • 利用闭包还可以把多参数的函数变成单参数的函数,即函数柯里化:
function make_pow(n) {
    return function (x) {
        return Math.pow(x, n);
    }
}

// 创建两个新函数:
var pow2 = make_pow(2);
var pow3 = make_pow(3);

pow2(5); // 25
pow3(7); // 343

// 直接调用
make_pow(2)(5); // 25
make_pow(3)(7); // 343
7. 箭头函数
  • 箭头函数基本上相当于一个匿名函数,形如(参数列表) => {函数体},当参数列表只有一个参数时,可以省略(),没有参数或者多个参数时都不能省略,当函数体内只有一条return语句时,可以把{}return一起省略,但是不能只省略{}return。例如:
var fn = (x) => {return x * x}; // 完整写法
fn(2); // 4
var fn = x => x * x; // 省略写法
fn(2); // 4

var fn = x => {x * x}; // 只省略return,不会报错,但是函数体内没有返回值
// 除非函数确实不需要返回值,否则执行将会得到undefined
fn(2); // undefined

var fn = x => return x * x; // 只省略{},这是错误的写法,报错:Uncaught SyntaxError: Unexpected token 'return'
fn(2);
  • 箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。例如:
var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象,不需要再使用var that = this;
        return fn();
    }
};
  • 由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2021); // 31
  • 此外箭头函数也没有自己的arguments对象:
var arguments = [1, 2, 3];
var arr = () => arguments[0];

arr(5); // 1

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的 arguments 对象. arguments[0] 是 n,即传给foo函数的第一个参数
  return f();
}

foo(1); // 2
foo(2); // 4
foo(3); // 6
foo(3,2);//6
  • 箭头函数不能用作构造器,不能使用new操作符,否则会报错TypeError
8. generator生成器
  • generator由function*定义,可以使用yield多次返回,也可以使用return结束执行。
  • 调用generator的一种方式是调用generator对象的next()方法,next()方法会执行generator的代码,遇到yield时会返回一个对象{value: x, done: true/false}并暂停generator的运行,返回的value就是yield的返回值,done表示这个generator是否已经执行结束。当执行到donetrue时,这个generator对象已经全部执行完毕,就不要再继续调用next()了,此时value就是return的返回值。例如:
function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

var f = fib(5); // 创建一个generator对象
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}
  • 调用generator的另一种方式是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done
for (var x of fib(10)) {
    console.log(x); // 依次输出0, 1, 1, 2, 3, 5, 8, 13, 21, 34 
}

七、对象

在JavaScript中,一切都是对象。

1. 标准对象
  • 常用标准对象包括:numberstringbooleanfunctionundefinedobject
  • 特别注意null的类型是objectArray的类型也是object,如果用typeof将无法区分出nullArray和通常意义上的object——{}
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'
2. 包装对象
  • 类似Java中的包装类,JavaScript也提供了包装对象,numberbooleanstring都有包装对象,包装对象的类型会变为object。因此使用包装对象与基本数据进行===比较时会得到false
  • 使用new Number()构造函数可以得到Number类型的包装对象,同理也可以得到booleanstring的包装对象。若不写new操作符,Number()Boolean()String()被当做普通函数,把任何类型的数据转换为numberbooleanstring类型(注意不是其包装类型)。例如:
var n1 = new Number('123'); // Number {123}
var n2 = new Number(123); // Number {123}
var n3 = Number(123); // 123

n1 == n2; // false  包装对象比较的是内存地址
n1 == n3; // true  值相等
n1 === n3; // false  类型不同,n1是object,n3是number
n3 === 123 //true

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

由以上可知:不要使用包装对象! 包装对象跟基本类型的值进行===比较时结果必然是false,并且两个值完全相同的包装对象进行比较时很有可能得到的是false

注意事项:

  • 不要使用new Number()new Boolean()new String()创建包装对象;
  • 使用parseInt()parseFloat()来转换任意类型到number
  • 使用String()来转换任意类型到string,或者直接调用某个对象的toString()方法,但是注意nullundefined没有toString()方法,特别要注意number对象字面量直接调用toString()会报错:
var n = null;
var u; // undefined
n.toString(); // Uncaught TypeError: Cannot read properties of null (reading 'toString')
String(n); // 'null'
u.toString(); // Uncaught TypeError: Cannot read properties of undefined (reading 'toString')
String(u); // 'undefined'

var n = 123;
n.toString(); // '123'
// .会被当成是小数点而报错
123.toString(); // Uncaught SyntaxError: Invalid or unexpected token
// 注意是两个点,第一个点是小数点,相当于(123.0).toString()
123..toString(); // '123'
(123).toString(); // '123'
  • typeof操作符可以判断出numberbooleanstringfunctionundefined,判断其他类型的对象无法得到具体类型,只能得到object
  • 判断Array要使用Array.isArray(arr)
  • 判断null要使用myVar === null
  • 判断某个全局变量是否存在用typeof window.myVar === 'undefined'
  • 函数内部判断某个变量是否存在用typeof myVar === 'undefined'
3. 常用标准对象

Date

var now = new Date(); // 获取系统当前时间
now; // Mon Oct 04 2021 22:03:25 GMT+0800 (中国标准时间)
now.getFullYear(); // 2021, 年份
now.getMonth(); // 9, 月份,注意月份范围是0~11,9表示十月
now.getDate(); // 4, 表示4号
now.getDay(); // 1, 表示星期一
now.getHours(); // 22, 24小时制
now.getMinutes(); // 3, 分钟
now.getSeconds(); // 25, 秒
now.getMilliseconds(); // 70, 毫秒数
now.getTime(); // 1633356205070, 以number形式表示的时间戳

var d1 = new Date(2021, 10, 4, 22, 10, 30, 123);
d1; // Thu Nov 04 2021 22:10:30 GMT+0800 (中国标准时间)

// Date.parse()可以解析一个符合ISO 8601格式的字符串来获取时间戳
// 时间戳是一个自增的整数,它表示从1970年1月1日零时整的GMT时区开始的那一刻,到对应时间的毫秒数
// 时间戳可以精确地表示一个时间,并且与时区无关
var d2 = Date.parse('2021-10-04T22:10:30.123+08:00');
d2; // 1633356630123
var d3 = new Date(d2); // 通过时间戳即可获取Date对象
d3.toLocaleString(); // '2021/10/4 下午10:10:30',本地时间(北京时区+8:00),显示的字符串与操作系统设定的格式有关
d3.toUTCString(); // 'Thu, 04 Nov 2021 14:10:30 GMT',UTC时间,与本地时间相差8小时
Date.now(); // 1633357426941  获取当前时间戳

注意:

  • 当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设定为任何值;
  • JavaScript的Date对象月份值从0开始,0~11表示一到十二月;
  • 使用Date.parse()时传入的字符串使用实际月份01~12,转换为Date对象后getMonth()获取的月份值为0~11

RegExp

// 有2种方式创建正则表达式
var re1 = /^\d{3}\-\d{3,8}$/; // 使用2个/包裹起来的正则表达式字面量
var re2 = new RegExp('^\\d{3}\\-\\d{3,8}$'); // 通过new RegExp('pattern')创建一个RegExp对象
// 这两种方式创建的正则表达式在使用上没有区别
re1; // /^\d{3}\-\d{3,8}$/
re2; // /^\d{3}\-\d{3,8}$/

// 通过test()方法来测试给定的字符串是否符合条件
re1.test('010-12345'); // true
re1.test('010-1234x'); // 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']  有, ; 也可以分割

// 用正则表达式提取子串,()表示要提取的分组(Group)
var re3 = /^(\d{3})-(\d{3,8})$/; // 定义2个组
// 在RegExp对象上用exec()方法提取出子串来
// 第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。
re3.exec('010-12345'); // ['010-12345', '010', '12345']
re3.exec('010 12345'); // null  匹配失败时返回null
var re4 = /^([01][0-9]|2[0-3]|[0-9])\:([0-5][0-9]|[0-9])\:([0-5][0-9]|[0-9])$/; // 3个组,用于匹配时间
re4.exec('19:05:30'); // ['19:05:30', '19', '05', '30']

// 默认是贪婪匹配
var re5 = /^(\d+)(0*)$/;
re5.exec('102300'); // ['102300', '102300', '']
// 在量词*、+、? 或 {} 的后面紧跟一个?,可以使量词变为非贪婪匹配
var re6 = /^(\d+?)(0*)$/;
re6.exec('102300'); // ['102300', '1023', '00']

// 使用修饰符(flag)来全局匹配,查找所有匹配而非在找到第一个匹配后停止
var s = 'JavaScript, VBScript, JScript and ECMAScript';
var re7 = /[a-zA-Z]+Script/g; // 使用修饰符的方式为 var re = /pattern/flags
var re8 = new RegExp('[a-zA-Z]+Script', 'g'); // 构造函数则是 var re = new RegExp("pattern", "flags")
re7.exec(s); // ['JavaScript']
re7.lastIndex; // 10,上次匹配到的最后索引
re7.exec(s); // ['VBScript']
re7.lastIndex; // 20
re7.exec(s); // ['JScript']
re7.lastIndex; // 29
re7.exec(s); // ['ECMAScript']
re7.lastIndex; // 44
re7.exec(s); // null,直到结束仍没有匹配到
re7.lastIndex; // 0,查找完后自动重置为0
s.match(re7); // ['JavaScript', 'VBScript', 'JScript', 'ECMAScript'],全局匹配使用match返回所有匹配到的字符串
var re9 = /[a-zA-Z]+Script/; // 不使用全局匹配
s.match(re9); // ['JavaScript'],不使用全局匹配时仅返回匹配到的第一个字符串

注意:

  • 使用/pattern/的形式创建的是正则表达式字面量,脚本加载后,正则表达式字面量就会被编译。当正则表达式保持不变时,使用此方法可获得更好的性能;
  • 若使用构造函数new RegExp('pattern')创建RegExp对象,正则表达式在脚本运行过程中被编译,如果正则表达式将会改变,或者它将会从用户输入等来源中动态地产生,就需要使用构造函数来创建正则表达式。需要特别注意,使用构造函数时字符串中的\需要转义写成\\

JavaScript正则表达式常用匹配规则表:

字符描述
^匹配输入的开始
$匹配输入的结束
[abc]匹配方括号之间的任何字符
[^abc]匹配任何不在方括号之间的字符
[0-9]匹配从09的数字
x|y匹配x或者y
*匹配前一个表达式 0 次或多次,等价于 {0,}
+匹配前面一个表达式 1 次或者多次,等价于 {1,}
?匹配前面一个表达式 0 次或者 1 次,等价于 {0,1}。如果紧跟在任何量词 *+?{} 的后面,将会使量词变为非贪婪
.(小数点)默认匹配除换行符之外的任何单个字符
{n}n是一个正整数,匹配了前面一个字符刚好出现了n次
{n,}n是一个正整数,匹配前一个字符至少出现了n次
{n,m}nm都是整数。匹配前面的字符至少n次,最多m次。如果 n 或者 m 的值是0, 这个值会被忽略
\d匹配一个数字,等价于[0-9]
\D匹配一个非数字字符,等价于[^0-9]
\w匹配一个单字字符(字母、数字或者下划线),等价于[A-Za-z0-9_]
\W匹配一个非单字字符,等价于 [^A-Za-z0-9_]
\n匹配一个换行符 (U+000A)
\s匹配一个空白字符,包括空格、制表符、换页符和换行符
\S匹配一个非空白字符
\转义字符,匹配特殊字符时需要转义,例如要匹配*需要使用\*

修饰符表:

修饰符描述
g全局搜索
i不区分大小写搜索
m多行搜索,^$匹配输入字符串中的每一行的开始或结束,而不是整个字符串的开始或结束
s允许.匹配换行符
u使用unicode码的模式进行匹配
y执行“粘性(sticky)”搜索,匹配从目标字符串的当前位置开始

JSON

JSON规定字符串必须用双引号"",Object的键也必须用双引号""

  • JSON.stringify()
    • JSON.stringify()方法将一个JavaScript对象或值转换为JSON字符串。
    • 方法原型:JSON.stringify(value[, replacer [, space]])
      • value:将要序列化成 一个 JSON 字符串的值。
      • replacer:如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为null或者未提供,则对象所有的属性都会被序列化。
      • space:指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格,上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为null),将没有空格。
    • 转换的值如果有toJSON()方法,则该方法定义哪些值将被序列化。
    • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
    • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
    • undefined、任意的函数以及symbol值,在序列化过程中会被忽略。
    • 函数、undefined被单独转换时,会返回undefined
    • 所有以symbol为属性键的属性都会被完全忽略掉,即便replacer参数中强制指定包含了它们。
    • NaNInfinity格式的数值及null都会被当做null
    • 不可枚举的属性不会被序列化。
var xiaoming = {
    name: '小明',
    age: 14,
    gender: true,
    grade: null,
    'middle-school': '\"W3C\" Middle School',
    skills: ['JavaScript', 'Java']
};

JSON.stringify(xiaoming);
// '{"name":"小明","age":14,"gender":true,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java"]}'
JSON.stringify(xiaoming, null, 2);
// '{\n  "name": "小明",\n  "age": 14,\n  "gender": true,\n  "grade": null,\n  "middle-school": "\"W3C\" Middle School",\n  "skills": [\n    "JavaScript",\n    "Java"\n  ]\n}'
JSON.stringify(xiaoming, ['name', 'skills'], 2);
// '{\n  "name": "小明",\n  "skills": [\n    "JavaScript",\n    "Java"\n  ]\n}'
function convert(key, value) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    }
    return value;
}
JSON.stringify(xiaoming, convert, 2);
// '{\n  "name": "小明",\n  "age": 14,\n  "gender": true,\n  "grade": null,\n  "middle-school": "\"W3C\" MIDDLE SCHOOL",\n  "skills": [\n    "JAVASCRIPT",\n    "JAVA"\n  ]\n}'
xiaoming.toJSON= function () {
    return { // 只输出name和age,并且改变了key:
        'Name': this.name,
        'Age': this.age
    };
};
JSON.stringify(xiaoming);
// '{"Name":"小明","Age":14}'  
  • JSON.parse()
    • JSON.parse()方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。
    • 方法原型:JSON.parse(text[, reviver])
      • text:要被解析成 JavaScript 值的字符串。
      • reviver:转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。
    • 传入的JSON对象不允许以,作为结尾
JSON.parse('[1,2,3,true]'); // [1, 2, 3, true]
JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14}
JSON.parse('true'); // true
JSON.parse('123.45'); // 123.45
JSON.parse("[1, 2, 3, 4, ]"); // Uncaught SyntaxError
JSON.parse('{"foo" : 1, }'); // Uncaught SyntaxError

// 使用reviver
var obj = JSON.parse('{"name":"小明","age":14}', function (key, value) {
    if (key === 'name') {
        return value + '同学';
    }
    return value;
});
console.log(JSON.stringify(obj)); // {name: '小明同学', age: 14}
4. 构造函数
  • 为了区别于普通函数,习惯上构造函数首字符大写。
  • 构造函数的this绑定新创建的对象,并且默认返回this,无需写return this语句。
  • 调用构造函数必须使用new操作符,否则在非严格模式下this将绑定window对象。
5. 原型prototype
  • 只有函数才有prototype属性。
  • 每个函数的prototype属性中都有一个constructor属性,这个constructor保存了当前函数的构造器,而对象的constructor指向构造出当前对象的函数的引用。顶级函数由new Function()构造。
function Person() {}
Person.prototype.constructor; // ƒ Person() {}
Person.constructor; // ƒ Function() {}
  • 将属性或方法定义在构造函数的原型prototype中,可以使构造出来的对象的对应属性或方法均为同一个。
function Person(name,age){
    this.name = name;
    this.age = age;
    this.__proto__.count++;
}
Person.prototype.count=0;
Person.prototype.sayHello=function(){
    console.log(this.name + "say hello");
}
var girl = new Person("bella",23);
girl.name; // bella
girl.count; // 1
var boy = new Person("alex",23);
boy.name; // alex
girl.count; // 2,girl的count和boy的count是同一个
boy.count; // 2
console.log(girl.sayHello === boy.sayHello);  //true
  • 所有对象都具有__proto__属性,该属性指向创建这个对象的构造函数的原型prototype对象。
  • JavaScript通过__proto__属性实现原型链。
boy.__proto__ === Person.prototype; // true
boy.__proto__.__proto__ === Object.prototype; // true
boy.__proto__.__proto__.__proto__; // null,原型链到达尽头
6. 继承

参考:
阮一峰的网络日志构造函数的继承非构造函数的继承
廖雪峰的官方网站class继承

八、错误

  • 如果在一个函数内部发生了错误,它自身没有捕获,错误就会被抛到外层调用函数,如果外层函数也没有捕获,该错误会一直沿着函数调用链向上抛出,直到被JavaScript引擎捕获,代码终止执行。
  • try...catch...finally...语句中的catchfinally块允许省略其中一个,但是写程序时不要省略catch,因为try...finally...的结构不会捕获错误,没有捕获的错误会使程序停止运行。程序抛出错误时在catch中一定要进行处理或者输出信息,否则将无法得知程序中有错误。
  • throw语句允许抛出任意类型的对象,但是最好还是抛出Error对象。
  • 如果try块内有异步函数,则catch块可能捕获不到异步函数中的错误,因为捕获错误时,异步函数还未执行。这种情况下需要在异步函数内部添加try...catch...块。

九、BOM

  • window对象在BOM中可以表示浏览器对象,拥有一些可以很容易获取到浏览器各种信息的属性。
  • window对象的innerWidthinnerHeight属性可以用于获取浏览器窗口的内部宽度和高度,这个宽度和高度是页面的净宽高,不包括浏览器标题,菜单,标签等页面无关的元素,但是包括滚动条。outerWidthouterHeight属性则是整个浏览器窗口的宽高。
  • navigator对象用于获取浏览器的信息,但是navigator的信息可以很容易地被用户修改,所以JavaScript读取到的值不一定是正确的。判断浏览器是否支持某个属性时可以直接使用||短路运算。例如:
var width = window.innerWidth || document.body.clientWidth;
  • screen对象用于获取屏幕信息,screen.widthscreen.height可以获取到屏幕的分辨率,screen.colorDepth可以获取色深。注意:screen对象获取到的分辨率是操作系统设置的分辨率,而并非硬件分辨率;获取的色深是表示一个像素的颜色所需要的比特数,而不是显示器面板色深,例如一个常见的硬件规格为 8bit 色深的RGB显示器,screen.colorDepth获取到的色深则是24bit,因为一个像素是RGB三个通道组成的。
  • location对象可以通过href属性获取当前URL,location.href也可以用来设置当前URL,可以使用location.reload()刷新当前页面,location.reload(true)可以强制刷新页面。
var url = location.href; // 获取当前页面URL
location.href = 'https://cn.bing.com/'; // 设置当前URL为必应并跳转至该页面
  • document对象表示当前页面,document对象是整个DOM树的根节点。注意,documentcookie属性可以用于获取当前页面的cookie,而这可能会导致用户信息泄露,因此服务器端可能会给某些cookie设置httpOnly,设置了httpOnly的cookie将不能被JavaScript读取。
  • history对象保存了当前浏览器窗口的历史记录,调用history对象的back()forward ()方法,相当于用户点击了浏览器的“后退”或“前进”按钮。不过由于AJAX的大量应用,直接使用history.back()会降低用户体验,现代网站不应该再使用history对象了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值