《JavaScript高级程序设计》读书笔记
第一章 JavaScript 简介
1.2 js 实现
- 一个完成的 js 实现由 3 个不同部分组成:核心(ECMAScript)、文档对象模型(DOM)、浏览器对象模型(BOM)
- 常见的 Web 浏览器知识 ECMAScript 实现的宿主环境之一,其他环境包括 Node、Adobe Flash
- DOM:是针对 XML 经过扩展用于 HTML 的程序编程 API
- BOM:控制浏览器显示的页面以外部分
第二章 在 HTML 中使用 JavaScript
2.1 <script>标签
下面两个属性可以控制 script 加载,它们不能严格保证执行顺序:
- async:不阻塞页面,下载并且执行脚本
- defer:脚本延迟到文档被完全解析和显示后再执行。
script 脚本中不要嵌入出现"</script>" 字符串,会被错误识别为结束标签。正确写法是:"<\/script>"。
如果 script 标签中既有代码内容,并且也引入了外部脚本(src 属性)。浏览器只会执行外部脚本。
2.2 可扩展超文本标记语言 XHTML
XHTML 编写比 HTML 更严格,例如 > 等符号都需要转义。为了保证 js 正常运行,用 CDATA 来包裹。
下面代码在不兼容 xml 的浏览器可以平稳退化:
1
2
3
4
5
6
7
8
9
<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
if (a < b) {
console.log("a is less than b");
}
}
//]]>
</script>
2.4 <noscript> 元素
对于不支持 js 的浏览器,此标签可平稳退化。
1
2
3
4
5
<body>
<noscript>
<p>请启用JavaScript</p>
</noscript>
</body>
第三章 基本概念
3.1 语法
针对 ES3 的不确定行为,ES5 增加了严格模式,它是“编译指示”,用来告知 Js 引擎切换到严格模式,需要在代码顶部添加:"use strict";
3.2 关键字和保留字
保留字是之后可能被用作关键字的标识符。比如super,在 es6 中被用在了子类的构造函数中。
3.3 变量
var 声明的变量存在声明提升,如下:
1
2
3
4
5
6
7
8
9
var a = 1;
function test() {
console.log(a);
var a = 2;
a = 3;
}
test();
由于变量声明提升以及函数作用域,相当于以下代码:
1
2
3
4
5
6
7
8
9
10
var a = 1;
function test() {
var a = undefined;
console.log(a);
a = 2;
a = 3;
}
test();
因此,输出结果是 undefined
3.4 类型
3.4.1 null 和 undefined
null 和 undefined 不相同,区别如下:
- null:空对象指针,
typeof null返回"object",常用于定义空变量 - undefined:未定义,变量只声明时,默认赋值
undefined
3.4.2 8 和 16 进制
number类型:
- 8 进制:0 开头,例如 070
- 16 进制:0x 开头,例如 0x1f
- 科学计数法:1ex,例如 1e2 = 100
所有 8 和 16 进制值在运算时,都会被转化为 10 进制。
3.4.3 特殊数字
Number.MIN_VALUE 和 Number.MAX_VALUE 分别返回最小值和最大值。超出范围的会被转化为 Infinity 。
不合法的数,比如 1/0 ,会返回 NaN,需要用 isNaN 判断。对于对象,isNaN 先调用 valueOf ,再掉用 toString 。
1
2
3
4
5
6
7
8
9
10
const validNum = {
valueOf: function() {
return 1;
},
toString: function() {
return "str";
}
};
console.log(isNaN(validNum)); // output: false
3.4.4 字符串转数字
parseInt 应该在第二个参数指明进制。
3.4.5 字符串
字符串变量的值是不可变的,当改变值时,会销毁之前的字符串,然后用包含新值的字符串填充变量。
调用数值的 toString 方法,给定参数代表进制。
特殊编码:
\xnn:以 16 进制代码 nn 表示字符\unnnn:以 16 进制代码 nnnn 表示 Unicode 字符
1
2
console.log("\x41"); // A
console.log("\u03a3"); // Σ
3.4.6 Object 类型
Object 实例都有以下属性:
- constructor: 指向创建对象的函数
- hasOwnProperty
- obj1.isPrototypeOf(obj2): obj1 是不是在 obj2 的原型链上
- propertyIsEnumerable(propName): propName 能否用 for-in 枚举
关于 isPrototypeOf:
1
2
3
4
5
6
7
8
9
function Demo() {}
var o = {};
var demo = new Demo();
console.log(o.isPrototypeOf(demo)); // output: false
// 将o放在demo实例的原型链上
demo.__proto__ = Demo.prototype = o;
console.log(o.isPrototypeOf(demo)); // output: true
对于 BOM、DOM 等宿主环境提供的对象,可能并不继承 Object,不具有以上通性。
3.5 操作符
1、位操作
~: 按位非。~110 => 001&: 按位与。|: 按位或。^: 异或操作。位数相同返回 0,不同返回 1。<<: 左移>>: 默认情况,有符号右移,保留符号位(符合正常逻辑)>>>: 无符号右移,在移动时候忽略符号位。
正因为移动时候忽略符号位,因此例如 -64 = 111111..11100000,负数的补码会被当做正数的二进制码。
2、布尔操作
一般直接使用 !! 进行转化。
3、逗号操作符
1
2
3
4
5
6
var num1 = 1,
num2 = 2,
num3 = 3; // 多变量声明
var num = (3, 2, 1); // 从右边开始解析,返回 1
console.log(num); // output: 1
3.6 语句
3.6.5 for-in 语句
精准迭代,枚举对象属性。但是效率很低,而且输出的属性名的顺序不确定。
在执行前,需要检测对象是否为 null 或者 undefined,否则 es3 会报错。
3.6.6 label 语句
与break 和 continue 联合使用,主要用于多层嵌套循环的流程控制。
配合 break,直接跳出指定的 label :
1
2
3
4
5
6
7
8
9
10
11
var num = 0;
outermost: for (var i = 0; i < 10; ++i) {
for (var j = 0; j < 10; ++j) {
if (i === 5 && j === 5) {
// i, j为5的时候,结束循环
break outermost;
}
++num;
}
}
console.log(num); // 55
配合 continue,直接跳出指定的 label :
1
2
3
4
5
6
7
8
9
10
var num = 0;
outermost: for (var i = 0; i < 10; ++i) {
for (var j = 0; j < 10; ++j) {
if (i === 5 && j === 5) {
continue outermost;
}
++num;
}
}
console.log(num); // 95
开启调试后会发现,当 i 和 j 为 5 的时候,跳到了 outermost,并且保持了 i 和 j 的变量值。
外层循环导致 i 变为 6,j 清零。
3.6.8 with 语句
设置代码作用域到指定对象中,会导致性能下降。
1
2
3
4
5
6
7
const obj = {
a: 1
};
with (obj) {
console.log(a); // 1
}
3.7 函数
arguments 是类数组对象,严格模式下不能重写或者重新定义其中的值。
arguments.callee 指向函数自身,用于编写递归函数。
注意:js 的函数没有重载。ts 可以重载,但是也只是多类型声明,不符合传统意义的函数重载。
第四章 变量、作用域和内存问题
4.1 基本类型和引用类型的值
4.1.2 赋值
复制函数:
1
2
var obj1 = new Object(); // obj1 保存的是副本,不过这个副本是指向实例的一个指针
var obj2 = obj1;
ECMAScript 中所有函数的参数都是按值传递,对于复杂类型,副本就是指向它的指针。
4.1.4 检测类型
基本数据类型:typeof;对象类型检测:instanceof
4.2 执行环境和作用域
延长作用域链的情景:
try-catch中的catch:作用域链前端新增错误对象with:作用域链前端新增指定对象- 函数闭包
4.3 垃圾回收(GC)
4.3.1 标记清除和引用计数
浏览器的实现有两种:
- 标记清除:所有变量打标记;去掉环境中变量的标记,以及被环境中变量引用变量的标记;之后,清除还有标记的变量。
- 引用计数:跟踪每个变量引用次数,被引用的变量就加 1;如果此变量又取了另一个变量,减 1。
1
2
3
4
5
6
7
const value = 1; // 引用0
const copy = value; // 引用+1
const obj = {
copy // 引用 + 1
};
obj.copy = null; // 引用 -1
// 最后,引用次数为1
引用计数无法处理“循环引用”的情况,例如:
1
2
3
4
5
6
7
8
9
function problem() {
const obja = {},
objb = {};
obja.prop = objb; // objb的引用次数和obja的引用次数都+1
objb.prop = obja; // objb的引用次数和obja的引用次数再+1
// obja 和 obj2 的引用次数均是2
// 变量永远不会被清除,造成内存泄漏
}
4.3.3 性能优化
在优化性能问题上,IE6 根据固定的内存分配量来触发 gc。但是如果脚本中声明了很多变量,并且都没有被释放,那么一直会达到触发标准,gc 会高频率触发,效率低下。
es7 做出了改进:临界值是动态计算的。如果一次垃圾回收的内存量低于 15%,那么临界值会翻倍;如果高于 85%,重置临界值。
4.3.4 管理内存
解除引用:不使用的变量,设置为null。
解除引用不意味变量内存回收,而是让其脱离执行环境,方便下次 gc 回收。
5. 引用类型
ECMAScript 是面向对象语言,但不是传统的面向对象。提供构造函数,专门对接传统对象编程。
5.1 Object 类型
new Object() 和 {} 声明等效。
5.2 Array 类型
创建有Array和 []2 种方式。
length 是可读写的,置 0 可以清空数组。
5.2.1 数组检测
请用 Array.isArray 检测数组。instanceof 不适用于网页包含多个框架,2 个运行环境,从一个向另一个传入数组构造函数,严格意义上并不相等。
1
2
3
4
5
6
7
<script>
const { frames } = window;
const length = frames.length;
xArray = frames[length - 1].Array;
const arr = new Array();
console.log(arr instanceof xArray); // false
</script>
5.2.3 栈和队列
- 栈:
push&&pop - 队列:
push&&shift
5.2.6 操作方法
concat:参数会被自动展开
1
2
const colors = [1];
const colors2 = colors.concat(2, [3, 4]); // [1, 2, 3, 4]
slice(star, end): 切片,返回新数组。
splice(start, count, …items):
- 删除:不需要第三个参数
- 插入:第二参数置 0
- 替换:第二个和第三个参数要用
5.3 Date 类型
Date.now() 和 new Date().gewNow() 等价。
Date.parse(string): 返回 string 代表的日期的毫秒数。年/月/日,请不要使用-连接!
Date 实例可以直接比较大小,因为valueOf返回毫秒数。
5.4 RegExp 类型
不推荐 new RegExp(string) 来声明正则,因为 string 是字符串,元字符需要双重转义。比如\n,就是\\n。
每个实例拥有以下属性:
- global:g
- ignoreCase: i
- multiline: m
- lastIndex: 搜索下一匹配项的字符位置
- source: 正则的字符串表示
5.5 Function 类型
代码求值时,js 引擎会将声明函数提升到源码顶部。
arguments上重要属性:
- length:参数长度
- callee: 函数自身引用
函数上重要属性:
- caller: 调用此函数的函数引用。全局访问返回 null
- length:函数希望接受的参数个数(不算默认参数)
1
2
3
4
5
6
7
8
9
function outer() {
inner();
}
function inner(a, b = 1) {
console.log(arguments.callee.caller === outer);
}
outer(); // true
inner.length; // 2 - 1 = 1
函数 prototype 属性无法枚举,不能用 for-in 枚举
- 可以使用
Object.getOwnPropertyNames,返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。 - 可以使用
Reflect.ownKeys,返回包括所有自身属性的属性名的数组
5.6 基本包装类型
num.toFixed(位数):自动舍入,返回字符串。
num.toExponential(位数):转化为科学计数法,返回字符串。
String.fromCharCode(…charcodes): 将字符编码转化为字符串。
String.charCodeAt(index): 将 index 的字符转化为字符编码。
5.7 单体内置对象
随机整数生成:
1
2
3
4
5
// [start, end]
function randomInt(start, end) {
const times = end - start + 1;
return Math.floor(Math.random() * times + start);
}
第六章 面向对象的程序设计
ECMA-62 对象定义:无序属性集合,其属性可以包括基本值、对象和函数。
6.1 理解对象
ECMA 有 2 种属性:数据属性和访问器属性。它们可以通过 Object.getOwnPropertyDescriptor 来读取。
1.数据属性
通过 Object.defineProperty(对象, 属性名, {属性: 值}) 来修改,可修改的属性是:configurable(是否可通过delete删除)、enumerable(能否 for-in 循环)、writable(能否修改)、value。
可以多次调用 api 修改上述属性,除了将 configurable 设置为 false。
2.访问器属性
访问器属性不包含数据值,也是通过 Object.defineProperty(对象, 属性名, {属性: 值}) 来修改。
可修改的属性是:configurable、enumerable、get、set。其中,只指定 get 不指定 set,那么就是不可写;反过来,不能读。
6.2 创建对象
6.2.1 理解原型对象
原型模式中,实例的 __proto__ 指向构造函数的 prototype,因此,构造函数.prototype.isPrototypeOf(实例)返回 true。
因为原型链有下端“屏蔽”上端的机制,可以通过逐步 delete 来暴露上端属性。
6.2.2 原型与 in 操作符
如果对象可以访问给定属性,那么 in 返回 true。
1
2
3
4
function Person() {}
Person.prototype.name = "student";
const person = new Person();
console.log("name" in person); // output
检测 prototype 是否位于 原型链上,而不位于实例上。
1
2
3
4
function hasPropertyInPrototype(object, prototype) {
// hasOwnProperty 是否位于实例上
return prototype in object && !object.hasOwnProperty(prototype);
}
6.2.3 自定义原型
(构造)函数的constructor 属性是自身,所以重写prototype的时候,需要注意:
1
2
3
4
5
6
7
8
function Person() {}
Person.prototype = {
name: "dongyuanxin"
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false, // Person.prototype.constructor 是不可枚举的
value: Person
});
6.2.4 动态原型
为了对应 OO 编程习惯,prototype 上属性在访问时动态创建:
1
2
3
4
5
6
7
8
9
function Person() {
this.name = "person";
if (typeof this.sayHello !== "function") {
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
}
6.2.6 稳妥构造函数
经常使用,尤其是在对原生对象做拓展时候,而且不能影响原有原型链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function PowerDate() {
const date = new Date();
date.format = () => {
const year = addZeroStr(date.getFullYear()),
month = addZeroStr(date.getMonth() + 1),
day = addZeroStr(date.getDate()),
hour = addZeroStr(date.getHours()),
minute = addZeroStr(date.getMinutes()),
second = addZeroStr(date.getSeconds());
return `${year}/${month}/${day} ${hour}:${minute}:${second}`;
};
// 可以new调用,因为return重置了返回值
return date;
}
6.3 继承
- 接口继承:继承方法签名
- 实现继承:继承实际方法
常见四种方法:JavaScript 基础知识梳理-下
第七章 函数表达式
7.2 闭包
闭包是指:有权访问另一个函数作用域中的变量的函数。作用域得到了延长。
一个经典问题:
1
2
3
4
5
6
7
8
9
10
11
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; ++i) {
result[i] = function() {
return i;
};
}
return result;
}
调用 result 中的函数,返回值均是 10。这是因为 var 不是块级作用域,闭包声明造成了内函数可以访问 createFunction 的作用域,并且在结束函数后,变量i的生命被延长了下来。例如,当调用 result[0] 的时候,就会访问并且返回 createFunction 中的 变量i的值。
如果将 var 换成 let,则不存在这个问题。虽然变量i生命被延长,也属于 createFunction作用域,但是let本身是“块级作用域”。也就是说,闭包中返回的i是当前循环下的i,没有发生污染。
7.3 模仿块级作用域
下面写法内存占用低,标记清除的gc在函数运行完,检测到不被使用,会立即销毁作用域链。
1
2
3
(function() {
// ...
})();
7.4 私有变量
利用闭包,可以很巧妙地实现静态私有变量、私有函数方法等。
1
2
3
4
5
6
7
8
9
(function() {
var name = ""; // 静态私有变量
return {
name() {
return name + "123";
}
};
})();
第八章 BOM
8.1 window 对象
双重角色:js 访问浏览器的 api + ECMAScript 规定的 global 对象。
8.1.1 全局作用域
定义在全局的变量不能被 delete, 定义在 window 上的属性可以被 delete。
8.1.2 窗口关系及框架
对于 window 的frames,为了保证兼容性,请使用:top.frames。因为top是绝对的。
除了top外,还有parent,在没有任何框架情况下,top === window。
最后,还有self。在 sw 中,常用 self 访问 window 上的 api。
8.1.3 窗口位置
跨浏览器取得窗口左边、上边的位置:
1
2
3
4
let leftPos =
typeof window.screenLeft === "number" ? window.screenLeft : window.screenX;
let topPos =
typeof window.screenTop === "number" ? window.screenTop : window.screenY;
此外,还有window.moveTo(x, y) 和 window.moveBy(offsetX, offsetY)两个方法移动位置。但是默认是禁用的。
8.1.4 窗口大小
窗口大小无法确定,但是可以跨浏览器获得页面视图大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let pageWidth = window.innerWidth,
pageHeight = window.innerHeight;
if (typeof pageWidth !== "number") {
if (document.compatMode === "CSS1Compat") {
// 是否是标准模式
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
} else {
// 是否是混杂模式
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}
此外,还有window.resizeTo(width, height) 和 window.resize(offsetWidth, offsetHeight)调整大小。但是默认是禁用的。
8.1.5 导航和打开窗口
window.open(href, windowName, paramsString): 最后一个参数形如 height=400,width=10。
这里有同域限制,并且返回的指针指向新开窗口,可以使用以上被禁用的方法。
对于一些浏览器插件,会禁用弹出,兼容代码如下:
1
2
3
4
5
6
7
8
9
10
11
let blocked = false;
try {
let wroxWin = window.open("http://baidu.com", "_blank");
if (!wroxWin) {
// 打开失败
blocked = true;
}
} catch (error) {
// 插件禁止后,会报错
blocked = true;
}
8.1.7 系统对话框
它们是浏览器决定的,是同步和模态的。显示的时候,会终止代码执行。
8.2 location 对象
location.href(最常用) 和 window.location 本质都是调用 location.assign()。
除此之外,修改 location 上的其他属性,也可以改变当前加载的页面,比如 location.hash='#setion'
以上方法,会在浏览器中生成新的历史记录。使用location.replace()方法,不会在浏览器中生成历史记录。
location.reload(true):强制重新加载。
8.3 navigator 对象
8.3.1 检测插件
navigator.plugins 存放插件信息:
1
2
3
4
5
6
7
8
9
10
// 通用检测方法
function hasPlugin(name = "") {
name = name.toLocaleLowerCase();
for (var i = 0; i < navigator.plugins.length; ++i) {
if (navigator.plugins[i].name.toLocaleLowerCase().indexOf(name) > -1) {
return true;
}
}
return false;
}
但由于 IE 浏览器的兼容,最好针对不同浏览器封装不同的插件检测方法。
8.3.2 注册处理程序
google 支持 registerProtocolHandler 自定义协议。比如打开https://www.baidu.com的控制台,在其中输入:
1
2
3
4
5
6
// 理论上是这样,但是效果不好
navigator.registerProtocolHandler(
"web+baidu",
"https://www.baidu.com/s?wd=%s",
"Baidu handler"
);
8.5 history 对象
history.go(): 任意跳转。数字代表前后跳转,字符串会自动找寻历史中最近的位置跳转。
history.length: 保存历史记录的数量。