【前端学习】《JavaScript高级程序设计》读书笔记
JavaScript 简介
- 一个完整的 JavaScript 实现应该由下列三个不同的部分组成:
- 核心(ECMAScript)
- 文档对象模型(DOM)
- 浏览器对象模型(BOM)
- 浏览器对象模型(BOM)可以访问和操作浏览器窗口,开发人员使用 BOM 可以控制浏览器显示的页面以外的部分
在 HTML 中使用 JavaScript
-
<script>
标签中有以下属性可使用:- async:表示应该立即下载脚本,但不应妨碍页面中的其他操作
- defer:表示脚本可以延迟到文档完全被解析和显示之后再执行
-
<script>
元素的src
属性可以包含来自外部域的 JavaScript 文件 -
为了减短白屏时间,现代 Web 应用程序中一般都把全部 JavaScript 引用放在
<body>
元素中页面的内容后面 -
如果在文档开始处没有发现文档类型声明,则所有浏览器都会默认开启混杂模式,但采用混杂模式不是什么值得推荐的做法。对于标准模式,可以通过使用下面任何一种文档类型来开启:
<!-- HTML 4.01 严格型 --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!-- XHTML 1.0 严格型 --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!-- HTML 5 --> <!DOCTYPE html>
基本概念
-
按照惯例,ECMAScript 标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个有意义的单词的首字母大写
-
在函数中,省略 var 操作符,可以定义得到一个全局变量。这不是推荐的做法,给未经声明的变量赋值在严格模式下会导致抛出 ReferenceError 错误
-
在严格模式下,不能定义名为 eval 或 arguments 的变量,否则会导致语法错误
-
从技术角度讲,函数在 ECMAScript 中是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过 typeof 操作符来区分函数和其他对象是有必要的
-
即使未初始化的变量会自动被赋予 undefined 值,但显式地初始化变量依然是明智的选择。如果能够做到这一点,那么当 typeof 操作符返回 undefined 值时,我们就知道被检测地变量还没有被声明,而不是尚未初始化
-
只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存 null 值。这样做不仅可以体现 null 作为空对象指针的惯例,而且也有助于进一步区分 null 和 undefined。
-
Number 类型转换为 false 的值为 0 和 NaN
-
十六进制字面值的前两位必须是 0x,后跟任何十六进制数字。其中,字母 A ~ F 可以大写,也可以小写。在进行算熟计算时都将被转换成十进制数值
-
永远不要测试某个特定的浮点数值。不精确是使用基于 IEEE754 数值的浮点计算的通病,ECMAScript 并非独此一家,其他使用相同数值格式的语言也存在这个问题
-
使用 Number() 把各种数据类型转换为数值确实有点复杂,下面是几个具体的例子
var num1 = Number("Hello world!"); // NaN var num2 = Number(""); // 0 var num3 = Number("000011"); // 11 var num4 = Number(true); // 1
-
Object
类型所具有的任何属性和方法也同样存在于更具体地对象中。Object的每个实例都具有下列属性和方法:- Constructor
- hasOwnProperty(propertyName)
- isPrototypeOf(object)
- propertyIsEnumerable(propertyName)
- toLocaleString()
- toString()
- valueOf()
-
同时使用两个逻辑非操作符,实际上就会模拟
Boolean()
转型函数的行为 -
通过
for-in
循环输出的属性名的顺序是不可预测的。具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异 -
使用
label
语句可以在代码中添加标签,以便将来使用。以下是label
语句的示例:start: for (var i = 0; i < count; i++) { alert(i); }
-
with
语句的作用是将代码的作用域设置到一个特定的对象中,例子如下:with (location) { var qs = search.substring(1); var hostName = hostname; var url = href; }
-
ECMAScript 函数不能像传统意义上那样实现重载。通过检查传入函数中参数的类型和数量并作出不同的反应,可以模仿方法的重载
变量、作用域和内存问题
-
在 ECMAScript 中,5种数据类型
Undefined
、Null
、Boolean
、Number
和String
都是按值访问的 -
只能给引用类型值动态地添加属性,以便将来使用
-
当从一个变量向另一个变量复制引用类型的值之后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量
-
ECMAScript 中所有函数的参数都是按值传递的,可以把 ECMAScript 函数的参数想象成局部变量。下面是一些例子:
function addTen(num) { num += 10; return num; } var count = 20; var result = addTen(count); alert(count); // 20,没有变化 alert(result); // 30 function setName(obj) { obj.name = "Nicholas"; } var person = new Object(); setName(person); alert(person.name); // "Nicholas" function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } var person = new Object(); setName(person); alert(person.name); // "Nicholas"
-
为检测引用类型的值,ECMAScript 提供了
instanceof
操作符 -
当执行流进入下列任何一个语句时,作用域链就会得到加长
try-catch
语句的catch
块with
语句
-
JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境时,就将这个变量标记为“进入环境”,而当变量离开环境时,则将其标记为“离开环境”
另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义时是跟踪记录每个值被引用的次数。这种方式涉及循环引用的问题
-
优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,通过将其值设置为
null
来释放其引用。它的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收
引用类型
-
在通过对象字面量定义对象时,实际上不会调用
Object
构造函数。与对象一样,在使用数组字面量表示法时,也不会调用Array
构造函数 -
ECMAScript 数组的大小是可以动态调整的,即可以随着数据的添加自动增长以容纳新增数据
-
为了解决
instanceof
操作符单一全局执行环境的问题,ECMAScript 5 新增了Array.isArray()
方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的 -
使用
join()
方法可以使用不同的分隔符来构建这个字符串。join()
方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串 -
ECMAScript 数组也提供了一种让数组的行为类似于其他数据结构的方法:
var colors = new Array(); var count = colors.push("red", "green"); alert(count); // 2 count = colors.push("black"); alert(count); // 3 // 模拟出栈,用pop()方法 var item = colors.pop(); alert(item); // "black" alert(colors.length); // 2 // 模拟出队,用shift()方法 var item = colors.shift(); alert(item); // "red" alert(colors.length); // 2
-
ECMAScript 还为数组提供了一个
unshift()
方法,与shift()
的用途相反 -
ECMAScript 的
sort()
方法接收一个比较函数作为参数。比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回 0,如果第一个参数应该位于第二个之后则返回一个正数。以下就是一个简单的比较函数:function compare(value1, value2) { if (value1 < value2) { return -1; // value1位于value2之前 } else if (value1 > value2) { return 1; // value1位于value2之后 } else { return 0; // 两个参数相等 } }
-
reverse()
和sort()
方法的返回值是经过排序之后的数组 -
splice()
方法主要的用法有 3 种:- 删除:可以删除任意数量的项,只需指定两个参数:要删除的第一项的位置和要删除的项数。例如,
splice(0, 2)
会删除数组中的前两项 - 插入:可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、0(要删除的项数)和要插入的项。例如,
splice(2, 0, "red", "green")
会从当前数组的位置 2 开始插入字符串red
和green
- 替换:可以向指定位置插入任意数量的项,例如
splice(2, 1, "red", "green")
- 删除:可以删除任意数量的项,只需指定两个参数:要删除的第一项的位置和要删除的项数。例如,
-
ECMAScript 5 为数组定义了 5 个迭代方法,每个方法都接收两个参数:要在每一项上运行的函数和运行该函数的作用域对象。这 5 个迭代方法分别是
every()
、filter()
、forEach()
、map()
、some()
。除此之外,还有两个缩小方法,reduce()
和reduceRight()
-
RegExp
的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息:global
:布尔值,表示是否设置了g
标志ignoreCase
:布尔值,表示是否设置了i
标志lastIndex
:整数,表示开始搜索下一个匹配项的字符位置,从 0 算起multiline
:布尔值,表示是否设置了m
标志source
:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回
-
RegExp
构造函数包含一些属性,这些属性分别有一个长属性名和一个短属性名 -
虽然
arguments
的主要用途是保存函数参数,但这个对象还有一个名为callee
的属性,该属性是一个指针,指向拥有这个arguments
对象的函数 -
ECMAScript 5 也规范化了另一个函数对象的属性:
caller
。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null
。 -
每个函数都包含两个非继承而来的方法:
apply()
和call()
,用于在特定的作用域中调用函数,实际上等于设置函数体内this
对象的值。除此之外,ECMAScript 5 还定义了一个方法:bind()
,这个方法会创建一个函数的实例,其this
值会被绑定到传给bind()
函数的值 -
Object
构造函数也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例 -
使用
new
调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的var value = "25"; var number = Number(value); // 转型函数 alert(typeof number); // "number" var obj = new Number(value); // 构造函数 alert(typeof obj); // "object"
-
String
类型定义了几个用于在字符串中匹配模式的方法:match()
、search()
、replace()
、localeCompare()
等 -
不属于任何其他对象的属性和方法,最终都是
Global
对象的属性和方法。事实上,没有全局变量或全局函数,所有在全局作用域定义的属性和函数,都是Global
对象的属性 -
Math.random()
方法返回介于 0 和 1 之间的一个随机数,不包括 0 和 1。套用下面的公式,就可以利用Math.random()
从某个整数范围内随机选择一个值
面向对象的程序设计
-
我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数
-
ECMAScript 中的数据属性共有 4 个描述其行为的特征:
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性[[Enumerable]]
:表示能否通过for-in
循环返回属性[[Writable]]
:表示能否修改属性的值[[Value]]
:包含这个属性的数据值
对于直接在对象上定义的属性,它们的
[[Configurable]]
、[[Enumerable]]
、[[Writable]]
、[[Value]]
特性都被设置为true
,而[[Value]]
特性被设置为特定的值。要修改属性默认的特性,必须使用 ECMAScript 5 的Object.defineProperty()
方法 -
访问器属性包含一对儿 getter 和 setter 函数。它有如下 4 个特性:
[[Configurable]]
:同上[[Enumerable]]
:同上[[Get]]
:读取属性时调用的函数[[Set]]
:写入属性时调用的函数
访问器属性必须使用
Object.defineProperty()
来定义。只指定 getter 意味着属性不能写,只指定 setter 意味着属性不能读 -
在 JavaScript 中,可以针对任何对象——包括 DOM 和 BOM 对象,使用
Object.getOwnPropertyDescriptor()
方法 -
ECMAScript 中一个使用构造函数模式创建对象的例子如下:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
-
任何函数,只要通过
new
操作符来调用,那它就可以作为构造函数 -
可以通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值。当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性
-
使用
hasOwnProperty()
可以检测一个属性是存在于实例中还是原型中。只有在实例中时,它才返回true
。但对in
来说,无论属性存在于实例中还是原型中,只要属性存在就返回true
-
在使用
for-in
循环时,返回的是所有能够通过对象访问的、可枚举的(enumerable)属性。要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的Object.keys()
方法。要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()
方法 -
实例与原型之间的连接只不过是一个指针,而非一个副本
-
实例中的指针仅指向原型,而不指向构造函数。重写原型对象会切断现有原型与之任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型
-
创建自定义类型的最常见模式,就是组合使用构造函数模式与原型模式。下面是一个例子:
function Person (name, job) { this.name = name this.job = job this.friends = ['Nakasuma Kasumi', 'Uehara Ayumu'] } Person.prototype = { constructor: Person, sayName: function() { console.log(this.name) } } const person1 = new Person('Emma Verde', 'School Idol') const person2 = new Person('Yuki Setsuna', 'School Idol') person2.friends.push('Osaka Shizuku') console.log('person1#friends', person1.friends) console.log('person2#friends', person2.friends) console.log('sayName#equal', person1.sayName === person2.sayName)
-
原型链有一种基本模式,其代码大致如下:
function SuperType() { this.property = true } SuperType.prototype.getSuperValue = function () { return this.property } function SubType() { this.subproperty = false } // Inherits SuperType SubType.prototype = new SuperType() SubType.prototype.getSubValue = function () { return this.subproperty } const instance = new SubType() console.log(instance.getSuperValue())
-
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后
-
在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做就会重写原型链
-
原型链最主要的问题来自包含引用类型值的原型。第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数
-
借用构造函数:可解决原型中包含引用类型值所带来的问题,可在子类型构造函数中向超类型构造函数传递参数。存在的问题是无法进行函数复用(方法都在构造函数中定义),且所有类型都只能使用构造函数模式(在超类型的原型中定义的方法对子类型而言不可见)
function SuperType() { this.colors = ['red', 'green', 'blue'] } function SubType() { SuperType.call(this) } const instance1 = new SubType() instance1.colors.push('black') console.log(instance1.colors) const instance2 = new SubType() console.log(instance2.colors)
-
组合继承:将原型链与借用构造函数的技术结合到一块,是 JavaScript 中最常用的继承模式。最大的问题是无论什么情况下,都会调用两次超类型构造函数。一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
function SuperType(name) { this.name = name this.colors = ['red', 'green', 'blue'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, age) { // Inherit property SuperType.call(this, name) this.age = age } // Inherit method SubType.prototype = new SuperType() SubType.prototype.sayAge = function () { console.log(this.age) }
-
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样
-
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。它的基本模式如下:
function inheritPrototype(subType, superType) { var prototype = object(superType.prototype) // 创建对象 prototype.constructor = subType // 增强对象 subType.prototype = prototype // 指定对象 }
函数表达式
-
arguments.callee
是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用 -
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中
-
闭包只能取得包含函数中任何变量的最后一个值
-
每个函数在被调用时,其活动对象都会自动取得两个特殊变量:
this
和arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止 -
闭包会引用包含函数的整个活动对象
-
用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示:
(function() { // 这里是块级作用域 })()
-
JavaScript 将
function
关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号 -
任何在函数中定义的变量,都可以认为是私有变量,包括函数的参数、局部变量和在函数内部定义的其他函数
-
构造函数中定义特权方法的基本模式如下:
function MyObject() { // 私有变量和私有函数 var privateVariable = 10 function privateFunction() { return false } // 特权方法 this.publicMethod = function() { privateVariable++ return privateFunction() } }
-
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法:
(function() { // 私有变量和私有函数 var privateVariable = 10 function privateFunction() { return false } // 构造函数 MyObject = function() { // 1. 函数声明只能创建局部函数,所以我们采用函数表达式 // 2. 初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量 } // 特权方法 MyObject.prototype.publicMethod = function() { privateVariable++ return privateFunction() } })
这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量
-
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function() { // 私有变量和私有函数 var privateVariable = 10 function privateFunction() { return false } // 特权/公有方法和属性 return { publicProperty: true, publicMethod: function() { privateVariable++ return privateFunction() } } }
-
有人进一步改进了模块模式,即在返回对象之前加入对其增强代码
var singleton = function() { // 私有变量和私有函数 var privateVariable = 10 function privateFunction() { return false } // 创建对象 var object = new CustomType() // 添加特权/公有属性和方法 object.publicProperty = true object.publicMethod = function() { privateVariable++ return privateFunction() } // 返回这个对象 return object }
-
递归函数应该始终使用
arguments.callee
来递归地调用自身,不要使用函数名——函数名可能会发生变化
BOM
-
全局变量不能通过
delete
操作符删除,而直接在window
对象上定义的属性可以 -
尝试访问未声明的变量会抛出错误,但是通过查询
window
对象,可以知道某个可能未声明的变量是否存在 -
调整浏览器窗口大小方法和移动窗口位置的方法类似,都有可能被浏览器禁用
-
在改变浏览器位置的方法中,最常用的是设置
location.href
属性 -
通过像下面这样测试
history.length
的值,可以确定用户是否一开始就打开了你的页面if (history.length == 0) { // 这应该是用户打开窗口后的第一个页面 }
客户端检测
-
能力检测的基本模式如下:
if (object.propertyInQuestion) { // 使用object.propertyInQuestion }
-
在可能的情况下,要尽量使用
typeof
进行能力检测
DOM
-
通过将节点的
nodeType
属性和Node
类型中定义的数值常量进行比较,可以很容易地确定节点的类型,例如:if (someNode.nodeType === Node.ELEMENT_NODE) { alert('Node is an element') }
-
NodeList
是有生命、有呼吸的对象,而不是在我们第一次访问它们的某个瞬间拍摄下来的一张快照 -
每个节点都有一个
parentNode
属性。包含在childNodes
列表中的所有节点都具有相同的父节点,且每个节点相互之间都是同胞节点。通过使用列表中每个节点的previousSibling
和nextSibling
属性,可以访问同一列表中的其他节点。父节点与其第一个和最后一个子节点之间也存在特殊关系。父节点的firstChild
和lastChild
属性分别指向其childNodes
列表中的第一个和最后一个节点 -
所有节点都有的一个属性是
ownerDocument
,使用它,我们可以直接访问文档节点 -
浏览器对
domain
属性有一个限制,即如果域名一开始是“松散的”(loose),那么不能将它再设置为“紧绷的”(tight) -
NodeList
及其“近亲”NamedNodeMap
和HTMLCollection
都是动态的集合。下列代码会导致无限循环:var divs = document.getElementByTagName('div'), i, div for (i = 0; i < divs.length; i++) { div = document.createElement('div') document.body.appendChild(div) }
-
每次访问
NodeList
对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作。