《JavaScript高级程序设计》读书笔记(二)

继续记录在读《JavaScript高级程序设计》时的零碎知识点。这几天在知乎上看到了别人使用 Electron 构建桌面应用,感觉还蛮有意思的。有兴趣的同学可以看看使用 Electron 构建桌面应用用 ReactJs 创建Mac版的 keep了解一下。也许在加深对JS的理解后,我也会选择用 Electron 做一个应用呢。Who knows?

这篇博文总结了《JavaScript高级程序设计》的4~5章:变量、作用域和内存问题,以及引用类型。

变量、作用域和内存问题

基本类型和引用类型的值

1.基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;引用类型的值是对象,保存在堆内存中。

2.当复制保存着对象的某个变量时,操作的是对象的引用。但在为对象添加属性时,操作的是实际的对象。

3.从一个变量向另一个变量复制基本类型的值时,会创建这个值的一个副本;从一个变量向另一个变量复制引用类型的值时,复制的是指向存储在堆中的一个对象的指针,复制之后两个变量指向同一个对象。

1
2
3
4
5
6
7
8
9
var n1 = 1;
var n2 = n1;
n1 = 2;
console.log(n2); // 1
var o1 = {};
var o2 = o1;
o1.name = 'Kyon';
console.log(o2.name); // Kyon

4.参数只能按值传递:

1
2
3
4
5
6
7
8
9
function setName(obj){
obj.name = 'Kyon';
obj = new Object();
obj.name = 'Huang';
}
var person = new Object();
setName(person);
console.log(person.name); // Kyon

在函数内部重写obj时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

5.typeof检测基本数据类型,instanceof检测引用类型(根据其原型链来识别)。

执行环境及作用域

6.每个执行环境(简称为环境,可以理解为作用域)都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。每个函数都有自己的执行环境,全局执行环境(在Web浏览器中为window对象)是最外围的一个执行环境。

7.当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返还给之前的执行环境。

8.当代码在一个环境中执行时,会创建变量对象的一个作用域链,用于保证对执行环境有权访问的所有变量和函数的有序访问(搜索)。作用域链的前端是当前执行的代码所在的变量环境,最后一个对象是全局执行环境的变量对象。

9.标识符解析:沿着作用域链一级一级地搜索标识符的过程。从作用域链的前端开始,逐级向后回溯,直到找到标识符为止(找不到通常导致错误)。

10.内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

11.延长作用域链:当执行流进入 try-catch 语句的 catch 块或 with 语句时,作用域链就会得到加长。这两个语句都会在作用域链的前端添加一个变量对象。对 with 语句来说,会将指定的对象添加到作用域链中;对 catch 语句,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

12.JavaScript 没有块级作用域,但有函数作用域(针对var)。

补充:在语法中的块级作用域是指if/else/for/while语句里2个大括号之间的部分。块级作用域里面定义的函数和变量在{}外部是可以被访问到的。但是函数就不行,比如你在函数体里面定义一个变量,那么函数执行完毕之后里面的变量就会直接被销毁,在函数体外部是不可能被访问到的。

再补充:ES6标准引入了新的关键字 let 和 const。它们都具有块级作用域。

垃圾收集

13.JavaScript 具有自动垃圾收集机制,原理:垃圾收集器按照固定的时间间隔(或代码执行中预定的收集时间)释放不再继续使用的变量所占用的内存。

14.最常用的垃圾收集方式是标记清除:垃圾回收器在运行时会给存储在内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记,而在此之后还有标记的变量被视为准备删除的变量,因为这些变量无法被访问到了。

此外,引用计数是另一种不太常用的垃圾收集策略,这种算法的思想是跟踪记录所有值被引用的次数。当代码中存在循环引用现象时,“引用计数”算法就会导致问题。JavaScript 引擎目前都不再使用这种算法,但IE访问非原生 JavaScript 对象(如DOM元素)时仍可能导致问题。

15.管理内存:优化内存占用的最佳方式为解除引用——一旦数据不再有用,通过将其值设置为null来释放其引用。解除引用的真正作用是让其值脱离执行环境,以便垃圾搜集器下次运行时将其回收,而并非自动回收该值所占的内存。

引用类型

引用类型的值(对象)是引用类型的一个实例。

Object类型

1.创建Object实例的两种方式:Object构造函数;对象字面量表示法。通过对象字面量定义对象时,实际不会调用Object构造函数(Firefox 2 及更早版本除外)。

1
2
3
4
5
6
7
8
var person = new Object();
person.name = "Kyon";
person.age = 19;
var person = {
name : "Kyon",
age : 19
};

2.访问对象属性常用点表示法,也可使用方括号表示法。除非必须用变量来访问属性,否则建议使用点表示法。

1
2
3
4
5
6
7
console.log(person.name); // 点表示法
console.log(person["name"]); // 方括号表示法
var propertyName = "name";
console.log(person[propertyName]);
console.log(person["first name"]);

Array类型

3.ECMAScript 数组的每一项可以保存任何类型的数据,并且大小可以动态调整。

4.创建数组的两种基本方式:使用Array构造函数;使用数组字面量表示法。通过数组字面量表示法时,实际不会调用Array构造函数(Firefox 3 及更早版本除外)。

1
2
3
4
5
6
var colors = new Array();
var colors = new Array(20);
var colors = new Array('red', 'blue', 'green');
var colors = [];
var colors = ['red', 'blue', 'green'];

5.length:利用length属性可以方便地在数组末尾添加新项:

1
2
var colors = ["red", "blue"];
colors[colors.length] = "green";

6.Array.isArray():ECMAScript新增Array.isArray()确定某个值是否数组,其解决了存在两个以上全局执行环境时instanceof检测结果出错的情况。

7.数组继承的toLocaleString()toString()valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。join()方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。

1
2
3
var colors = ["red", "green", "blue"];
console.log(colors.join(",")); //red,green,blue
console.log(colors.join("||")); //red||green||blue

8.栈方法和队列方法:

  • push()添加一项到数组末尾;
  • pop()移除数组末尾一项;
  • shift()移除数组第一项;
  • unshift()添加一项到数组前端。

9.重排序方法:

  • reverse()翻转数组项的顺序;
  • sort()默认将数组项转换成字符串后按升序排列(可以接收一个比较函数作为参数,第一个参数应位于第二个之前则返回一个负数)。
1
2
3
4
5
6
7
8
9
var a = [0, 1, 15, 10, 5];
a.sort();
console.log(a); // [0, 1, 10, 15, 5]
function compare(value1, value2){
return value1 - value2;
}
a.sort(compare);
console.log(a); // [0, 1, 5, 10, 15]

10.操作方法:

  • concat():添加项
1
2
3
var a1 = ['red', 'green', 'blue'];
var a2 = a1.concat('yellow', ['black', 'brown']);
console.log(a2); // ["red", "green", "blue", "yellow", "black", "brown"]
  • slice():截取
1
2
3
var a = ['red', 'green', 'blue', 'yellow', 'black', 'brown'];
console.log(a.slice(1), a.slice(1,4));
// ["green", "blue", "yellow", "black", "brown"]["green", "blue", "yellow"]
  • splice():删除插入替换
1
2
3
var a = ['red', 'green', 'blue', 'yellow', 'black', 'brown'];
console.log(a.splice(2, 1), a);
// ["green", "blue", "yellow", "black", "brown"]["green", "blue", "yellow"]

11.位置方法:indexOf()lastIndexOf()都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。indexOf()从数组开头(位置0)向后查找,lastIndexOf()从数组末尾向前查找。

1
2
3
4
5
var a = ["red", "purple", "orange", "green", "red", "yellow", "black", "brown"];
console.log(a.indexOf('red')); // 0
console.log(a.lastIndexOf('red')); // 4
console.log(a.indexOf('red', 1)); // 4
console.log(a.lastIndexOf('red', 1)); // 0

12.迭代方法:每个方法接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值。给定函数会接收三个参数:数组项的值该项在数组中的位置数组对象本身

  • every():如果给定函数对每一项都返回true,则返回true。
  • filter():返回给定函数会返回true的项组成的数组。
  • foreach():这个方法没有返回值。
  • map():返回每次函数调用的结果组成的数组。
  • some():如果给定函数对任一项返回true,则返回true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var a = [1, 2, 3, 4, 5, 4, 3, 2, 1];
var everyResult = a.every(function(item, index, array){
return (item > 2);
});
console.log(everyResult); // false
var filterResult = a.filter(function(){
return (item > 2);
});
console.log(filterResult); // [3, 4, 5, 4, 3]
var forEachResult = a.forEach(function(item, index, array){
console.log(item);
});
console.log(forEachResult); // undefined
var mapResult = a.map(function(item, index, array){
return (item * 2);
});
console.log(mapResult); // [2, 4, 6, 8, 10, 8, 6, 4, 2]
var someResult = a.some(function(item, index, array){
return (item > 2);
});
console.log(someResult); // true

13.归并方法:都会迭代数组的所有项,然后构建一个最终返回的值。都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。给定函数接收4个参数:前一个值、当前值、项的索引和数组对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [1, 2, 3, 2, 1];
var sum1 = a.reduce(function(prev, cur, index, array){
console.log(index); // 1 2 3 4
return prev + cur;
});
console.log(sum1); // 9
var sum2 = a.reduceRight(function(prev, cur, index, array){
console.log(index); // 3 2 1 0
return prev + cur;
});
console.log(sum2); // 9

Date类型

12.创建日期对象:月份基于0(一月是0,二月是1,以此类推)。

1
2
3
var d1 = new Date();
var d2 = new Date(2017, 2, 3, 15, 33, 33); // 2017年3月3日下午3点33分33秒

13.获取调用时的日期和时间和毫秒数,可以用来分析代码。

1
2
3
4
var start = Date.now();
doSomething();
var stop = Date.now();
var result = stop - start;

14.日期格式化方法:local表示以特定于地区的格式显示。

1
2
3
4
5
6
7
var d2 = new Date(2017, 2, 3, 15, 33, 33);
d2.toString(); // "Fri Mar 03 2017 15:33:33 GMT+0800 (CST)"
d2.toDateString(); // "Fri Mar 03 2017"
d2.toTimeString(); // "15:33:33 GMT+0800 (CST)"
d2.toLocaleString(); // "2017/3/3 下午3:33:33"
d2.toLocaleDateString(); // "2017/3/3"
d2.toLocaleTimeString(); // "下午3:33:33"

RegExp类型

15.pattern:正则表达式;flags:标志,表明正则表达式的行为。g全局模式,i不区分大小写,m多行模式。

1
2
var exp1 = /pattern/flags
var exp2 = new RegExp('pattern','flags');

16.RegExp实例方法:

  • exec()专门为捕获组而设计,返回第一个匹配项信息的数组(或 null),数组第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串。包含两个额外的属性:index 和 input。
1
2
3
4
5
6
7
8
9
var text = "mom and dad and baby";
var pattern = /mom( and dad( and baby)?)?/gi;
var matches = pattern.exec(text);
console.log(matches.index); // 0
console.log(matches.input); // "mom and dad and baby"
console.log(matches[0]); // "mom and dad and baby"
console.log(matches[1]); // " and dad and baby"
console.log(matches[2]); // " and baby"
  • test()接收一个字符串参数,在模式与该参数匹配的情况下返回 true,否则返回flase。
1
2
3
4
5
6
var text = "1234-56-7890";
var pattern = /\d{4}-\d{2}-\d{4}/;
if(pattern.test(text)){
console.log("The pattern was matched.");
}

17.RegExp构造函数属性:适用于作用域中的所有正则表达式,记录一些最近一次正则表达式操作的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
var text = "This has been a short summer";
var pattern = /(.)hort/g;
// Opera 不支持 input、lastMatch、lastParen 和 multiline 属性
// IE 不支持 multiline 属性
if(pattern.test(text)){
console.log(RegExp.input); // This has been a short summer
console.log(RegExp.leftContext); // This has been a
console.log(RegExp.rightContext); // summer
console.log(RegExp.lastMatch); // short
console.log(RegExp.lastParen); // s
console.log(); // false
}

Function类型

18.函数实际上是 Function 类型的实例,因此函数也是对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 使用函数声明语法
function f1 (n1, n2) {
return n1 + n2;
}
// 使用函数表达式
var f2 = function (n1, n2) {
return n1 + n2;
};
// 使用构造函数,不推荐(会导致解析两次代码,影响性能)
var f3 = new Function('n1', 'n2', 'return n1 + n2');

19.函数名是一个指向函数对象的指针,因此 ECMAScript 中没有函数重载。

20.函数声明与函数表达式:解析器会率先通过名为函数声明提升的过程,读取并将函数声明添加到执行环境中,使其在执行任何代码之前可用。

而函数表达式必须等到解析器执行到它所在的代码行,才会真正被解析执行。

1
2
3
4
5
6
7
8
9
10
console.log(sum(10, 10));
function sum(num1, num2){
return num1 + num2;
}
// 把函数声明改为等价的函数表达式,会在执行期间导致错误
console.log(sum(10, 10));
var sum = function(num1, num2){
return num1 + num2;
};

21.函数内部属性:callee、this、caller。

  • arguments 有 callee 属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。可用于递归中消除紧密耦合现象。
1
2
3
4
5
6
7
8
console.log(sum(10, 10));
function factorial(num){
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num-1);
}
}
  • this 引用的是函数据以执行的环境对象。
  • caller 属性保存着调用当前函数的函数的引用(如果在全局作用域中调用当前函数,它的值为 null)。
1
2
3
4
5
6
7
8
9
10
function outer(){
inner();
}
function inner(){
console.log(inner.caller);
// 为了实现更松散的耦合,也可以用 arguments.callee.caller 访问相同的信息
}
outer(); // 打印 outer() 函数的源代码

严格模式下访问 arguments.callee 和 arguments.caller 会导致错误,且不能为函数的 caller 属性赋值。

22.函数属性:length 和 prototype。

length 属性表示函数希望接收的命名参数的个数。

23.函数方法:apply()call()。用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值,区别仅在于接收参数的方式不同。

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中第二个参数可以是 Array 的实例,也可以是 arguments 对象。

call()方法中,传递给函数的参数必须逐个列举出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(num1, num2){
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]); // 传入数组
}
function callSum(num1, num2){
return sum.call(this, num1, num2);
}

apply()call()真正强大之处在于扩充函数赖以生存的作用域。

1
2
3
4
5
6
7
8
9
10
window.color = "red";
var o ={ color: "blue" };
function sayColor(){
console.log(this.color);
}
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue

在严格模式下,未制定环境对象而调用函数,则 this 值不会转型为 window。除非明确把函数添加到某个对象或者调用apply()call(),否则 this 值将是 undefined。

基本包装类型

24.三种基本包装类型:Boolean类型、Number类型、String类型。在读取模式下访问基本类型值时,就会创造对应的基本包装类型的一个对象,从而方便数据操作。

自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁(这意味不能在运行时为基本类型值添加属性和方法)。

单体内置对象

25.在所有代码执行之前,内置对象:Global 和 Math 已经实例化,开发人员不必显式地实例化内置对象。

在大多数ECMAScript实现中都不能直接访问 Global 对象,不过Web浏览器实现了承担该角色的 window 对象。全局变量和函数都是 Global 对象的属性。