《JavaScript高级程序设计》笔记
《JavaScript 高级程序设计》笔记
第 1 章 JavaScript 简介
JavaScript 诞生于1995年
一个完整的 JavaScript 由这三部分组成:
- ECMAScript (核心)
- DOM (文档对象模型)
- BOM (浏览器对象模型)
ECMAScript 的 宿主环境 包含 :
- 浏览器
- Node
- flash
ECMAScript 大致规定了下列组成部分:
- 语法
- 类型
- 语句
- 关键字
- 保留字
- 操作符
- 对象
第 2 章 在 HTML 中使用 JavaScript
<script>
标签有下列常见属性:- async 表示立即下载脚本,不妨碍页面的其他操作(异步下载)
- 无法保证每个 js 的顺序
- 用于互不依赖的 js
- 主要目的是不让页面等 js
- defer 延迟到文档完全解析和显示之后在执行
- 在
</html>
标签之后执行
- 在
- src 外部文件链接
- type 表示编写代码使用的脚本语言的内容类型
text/javascript
- async 表示立即下载脚本,不妨碍页面的其他操作(异步下载)
第 3 章 基本概念
语法,借鉴了 C 语言
- 区分大小写
- 标识符,指的是变量、函数、属性的名字
- 第一个字符必须是 字母、下划线、或美元符号
- 其他字符可以是字母、下划线、美元符号或数字
严格模式
'use strict'
数据类型
- 基本数据类型
- Undefined、Null、Boolean、Number、String
typeof
操作符,一般用于基本类型的检测,返回相应类型的字符串- “undefined”,"boolean","string","number","object","function"
- 复杂数据类型
- Object
- 基本数据类型
Null
类型- 表示空对象指针
- 只要意在保存对象的变量还没有真正保证对象,则保存
null
undefined
派生自null
,因此用==
比较返回true
浮点数,指的是数值中包含一个小数点,并且小数点后面至少有一位数字
NaN
- 任何涉及 NaN 的操作都返回
NaN
NaN
与任何值都不相等,包括NaN
本身
- 任何涉及 NaN 的操作都返回
数值转换
- 有 3 个函数可以把非数值转换为数值
- Number() 可以用于任何数据类型
- parseInt() 专门用于字符串转数值,用于解析整数
- parseFloat() 专门用于字符串转数值,用于解析浮点数
- 有 3 个函数可以把非数值转换为数值
字符串
- 数值、布尔值、对象、字符串值本身 都有 toString()方法,用于转为字符串
- undefined 和 null 没有 toString()方法,但可以使用 String()方法
Object 类型
对象其实就是一组数据与功能的集合
使用 Object()构造函数创建对象
var obj = new Object() // 同 obj = {}
每个对象实例都有下列属性和方法
constructor
指向用于创建当前对象的构造函数 。(对于上面例子而言,就是Object()
)hasOwnProperty(propertyName)
用于检查给定属性是否在当前对象实例中isPrototypeOf(Object)
用于检查传入的对象是否是当前对象的原型propertyIsEnumerable(propertyName)
用于检查给定的属性是否能够使用for-in
来枚举toLocaleString()
返回对象的字符串表示,该字符串与执行环境的地区对应toString()
返回对象的字符串表示valueOf()
返回对象的字符串、数值、布尔值表示。通常与toString()
返回值相同
所有对象都有以上属性和方法
操作符
- 一元操作符
- ++
- 前置与后置的区别是与其他数运算时,前置会先执行递增(减)再与其他数运算,后置会先与其他数运算再对自身执行递增(减)
- 布尔操作符
- 与 (&&)
- 短路操作,即如果第一个操作数能决定结果,就不会对第二个操作数求值
- 或 (||)
- 短路操作,即如果第一个操作数能决定结果,就不会对第二个操作数求值
- 非 (!)
- 先使用 Boolean()转成布尔值再取反
- 与 (&&)
- 一元操作符
语句(也称流控制语句)
if-else
do-while
后测试循环语句,在对表达式求值之前,循环体内的代码至少会被执行一次
var i = 0 do { i += 2 } while (i < 10) // 只要i小于10就会一直循环
while
前测试语句
var i = 0 while (i < 10) { i += 2 } // 只要i小于10就会一直循环
for
使用 while 循环做不到的,使用 for 循环同样做不到。就是说,for 循环只是把与循环有关的代码集合在一个位置
for (初始化; 条件; 循环后执行) { // ... }
break 立即退出循环
continue 退出当前单次循环
for- in 用来枚举对象的属性
函数
- 任何函数都可以在任何时候返回任何值
- 未指定返回值的函数会返回一个 undefined
- 通过 arguments 访问参数,它是一个类数组
第 4 章 变量、作用域和内存问题
变量包含两种类型的值:
- 基本类型值,指的是简单的数据段
- 引用类型值,指的是那些可能由多个值构成的对象
复制变量值
- 复制基本类型的变量值只是复制了该值的副本
- 复制引用类型的变量值时,同样也会将存储在变量对象的值复制一份放到新变量的空间中,不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。两个变量指向同一个对象。
传递参数
- 所有函数的参数都是按值传递的
检测类型
typeof
检测基础类型instanceof
检测引用类型- 变量 instanceof 构造函数(例:obj instanceof Object)
- 所有引用类型的值都是 Object 的实例
执行环境
- 在浏览器中,全局执行环境是
window
对象 - 每个函数都有自己的执行环境
- 在浏览器中,全局执行环境是
作用域链
- 当代码在一个环境中执行时,会创建变量对象的一个作用域链
- 作用域链的前端,始终是当前执行的代码所在环境的变量对象。
- 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含对象。这样一直延续到全局执行环境。
- 全局执行环境的变量对象始终都是作用域链中的最后一个对象。
垃圾收集
- JavaScript 具有自动垃圾收集机制
- 对于不再使用的变量打上标记,被打上标记的变量将在执行回收时被清除,释放内存空间。
- JavaScript 具有自动垃圾收集机制
第 5 章 引用类型
引用类型的值是引用类型的一个实例
引用类型就是一种数据结构,描述了一类对象所具有的属性和方法
创建 Object 实例的方式有两种:
var obj = new Object()
var obj = {} // 与 new Object() 相同,但实际不会调用Object构造函数
访问对象的属性有两种:
- 点表示法 和 方括号表示法
Array 类型
length 属性不是只读的
检测数组
value instanceof Array
Array.isArray(value)
转换方法
- toLocaleString() 与 toString()相同
- toString() 返回数组中每个值的字符串形式用逗号分隔拼接而成的字符串
- valueOf() 返回数组本身
join() 转换方法 (不改原数组)
接收一个参数,用作分隔符的字符串,然后返回包含所有数组项的字符串。
var arr = ['red', 'blue', 'green']
var arrStr = arr.join('|') // "red|blue|green"
arr.join() // "red,blue,green"
arr.join('') // "redbluegreen"
栈方法,后进先出 push() 、pop() (改变原数组)
- push() 向数组末尾添加成员,返回总长
- pop() 移除数组末尾一个成员,返回该成员
队列方法,先进先出 push()、shift() 、unshift() (改变原数组)
shift() 移除数组首个成员,返回该成员
unshift() 在数组前面添加成员,返回总长
push()和 shift() 形成队列方法
unshift 和 pop() 形成反方向队列方法
重排序方法 reverse() 反转 sort() 排序 (改变原数组)
- reverse() 反转数组项的顺序
- sort() 接收一个函数作为参数,函数接收两个参数。
- 自定义排序:函数内,如果第一个参数应该位于第二个之前,则手动返回一个负数,如果两个参数相等,手动返回 0,如果第一个参数应该位于第二个参数之后,则手动返回一个正数。
// 升序
arr.sort(function(a,b){
if(a < b) {
return -1
} else if (a > b) {
return 1
} else {
retunr 0
}
})
简写:
arr.sort((a, b) => {
return a - b // 升序, b-a 降序
})
操作方法 concat() 拼接,slice() 切片,splice() 移接
concat() 拼接数组 (不改原数组)
var arr1 = ['a', 'b'] var arr2 = arr1.concat('c', 'd') // ['a','b','c','d']
slice() 切片(不改原数组)
- slice(起始位置 [, 结束位置]) 返回起始位置到结束位置的成员,不包含结束位置。
var arr1 = [1, 2, 3, 4, 5] var arr2 = arr1.slice(1, 3) // [2,3] var arr3 = arr1.slice(2) // [3,4,5]
splice() 移接 (改变原数组)
- splice(起始位置, 要删除的个数,要插入的项)
- 要插入的项 可以有 0 个或多个
- splice() 始终返回一个数组,该数组成员中包含原始数组中被删除的项,如果没有则返回空数组。
- 数组最强大的方法,可用于删除、插入、替换操作
arr = [1, 2, 3, 4] arr.splice(1, 1) // [2] arr // [1,3,4] arr = [1, 2, 3, 4] arr.splice(2, 0, 'a') // [] arr // [1,2,'a',3,4] arr = [1, 2, 3, 4] arr.splice(3, 1, 'a') // [4] arr // [1,2,3,'a']
- splice(起始位置, 要删除的个数,要插入的项)
位置方法 indexOf()、lastIndexOf()
- 查找项的位置,没有则返回-1
- indexOf() 从前面开始找,lastIndexOf() 从后面开始找
迭代方法 every()、some() 、filter()、map() 、forEach() (都不会改变原数组)
5 个迭代方法,每个方法都接收两个参数:
- 在每项上运行的函数
- 运行函数的作用域对象
函数接收三个参数:
- 数组项的值
- 该项的索引
- 数组本身
every() 如果函数对每项都返回 true,则返回 true
例:判断数组每一项是否都大于 2
var numbers = [1, 2, 3, 2, 1] var result = numbers.every((item, index, array) => { return item > 2 }) result // false
some() 如果函数对任一项返回 true,则返回 true
例:判断数组是否包含大于 2 的值
var numbers = [1, 2, 3, 2, 1] var result = numbers.some((item, index, array) => { return item > 2 }) result // true
filter() 返回函数会返回 true 的项组成的数组
例:过滤掉数组中小于等于 2 的数
var numbers = [1, 2, 3, 4, 5] var result = numbers.filter((item, index, array) => { return item > 2 }) result // [3,4,5]
map() 返回每次函数调用的结果组成的数组
例:给数组每一项乘以 2
var numbers = [1, 2, 3, 4, 5] var result = numbers.map((item, index, array) => { return item * 2 }) result // [2,4,6,8,10]
forEach() 循环数组每一项,没有返回值
例:循环每一项
var numbers = [1, 2, 3, 4, 5] numbers.forEach((item, index, array) => { // 做一些操作 })
归并方法 reduce() reduceRight()
reduce() 归并为
reduceRight() 从右边归并
这两个方法都会迭代数组所有项,然后构建一个最终结果并返回
方法接收两个参数: 每一项上调用的函数,归并基础的初始值
函数接收 4 个参数:前一个的值、当前值、当前值的索引、数组对象
例:求数组每一项之和
var numbers = [1, 2, 3, 4, 5] var result = number.reduce((prev, cur, index, arr) => { return prev + cur }) result // 15
reduce()方法,第一次执行函数时,prev 是数组的第一项,cur 是数组的第二项
第 6 章 面向对象的程序设计
6.1 理解对象
- 对象的定义:无序属性的集合,其属性可以包含基本值、对象或者函数。
- 一组键值对,其中值可以是数据或函数
6.1.1 对象的属性
包含两种:数据属性 和 访问器属性
1.数据属性
- configurable 可配置性(限制 delete 删除属性和
Object.defineProperty()
方法是否起效。),默认 true - enumerable 可列举性,默认 true
- writable 可写性,默认 true
- value 值,默认 undefined
使用Object.defineProperty()
方法修改这些默认属性。接收三个值:属性所在的对象、属性名、描述符对象。
var person = {}
Object.defineProperty(person, 'name', {
writable: false,
value: 'xu'
})
console.log(person.name) // xu
person.name = 'gao' // 修改无效
console.log(person.name) // xu
使用此方法定义属性如未指定configurable
、enumerable
、writable
将默认false
。
2.访问器属性
包含两个函数:getter 函数和 setter 函数(这两个函数都是非必需)。读取时调用 getter,访问时调用 setter。
使用Object.defineProperty()
方法定义访问器属性。
var book = {
_year: 2020,
edition: 1
}
Object.defineProperty(book, 'year', {
get: function () {
return this._year
},
set: function (newValue) {
if (newValue > 2020) {
this._year = newValue
this.edition += newValue - 2020
}
}
})
book.year = 2021
console.log(book.edition) // 2
当 getter 函数和 setter 函数只指定其中一个时,另外一个将不能用。
6.1.2 定义多个属性
Object.defineProperties()
方法定义多个属性。接收两个参数: 要添加或修改属性的对象 、第二个参数是一个对象,其属性与第一个参数中的属性一一对应。
var book = {}
Object.defineProperties(book, {
_year: {
// 数据属性
writable: true,
value: 2004
},
edition: {
// 数据属性
writable: true,
value: 1
},
year: {
// 访问器属性
get: function () {
return this._year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
}
})
// 定义了三个属性,每个属性有不同的描述符
6.1.3 读取属性的特性(描述符对象)
Object.getOwnPropertyDescriptor()
方法获取描述符,接收两个参数: 属性所在的对象、属性名
var book = {}
Object.defineProperties(book, {
_year: {
// 数据属性
writable: true,
value: 2004
},
edition: {
// 数据属性
writable: true,
value: 1
},
year: {
// 访问器属性
get: function () {
return this._year
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue
this.edition += newValue - 2004
}
}
}
})
var descriptor = Object.getOwnPropertyDescriptor(book, '_year')
console.log(descriptor) // {value: 2020, writable: true, enumerable: false, configurable: false}
var descriptor = Object.getOwnPropertyDescriptor(book, 'year')
console.log(descriptor) // {enumerable: false, configurable: false, get: ƒ, set: ƒ}
6.2 创建对象
使用 Object 构造函数或对象字面量方式创建对象。缺点:使用同一个接口创建很多对象时,会产生大量重复代码。解决方案:使用工厂模式的一种变体。
6.2.1 工厂模式
用函数封装以特定接口创建对象的细节
function createPerson(name, age, job) {
var o = new Object() //显式创建对象
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o //返回对象
}
var person1 = createPerson('Greg', 22, 'Doctor')
person1 instanceof createPerson //false
工厂模式解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)
6.2.2 构造函数模式
类似 Object、Array 原生构造函数,在运行时自动出现在执行环境中。 自定义构造函数可以将它的实例标示为一种特定类型(胜过工厂模式)
function Person(name, age, job) {
this.name = name //将属性赋给this对象
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
var person1 = new Person('Greg', 22, 'Doctor')
person1 instanceof Object //true
person1 instanceof Person //true
【注】a. 构造函数应以大写字母开头,b. 创建 Person 实例需要使用 new 操作符,c. 实例对象包含 constructor 属性(值是该构造函数)
构造实际步骤:
- (1)创建一个新对象
- (2)将构造函数的作用域赋给新对象(this 指向新对象)
- (3)执行构造函数中代码
- (4)返回新对象
将构造函数当做函数
- a. 当做构造函数,使用 new 调用
- b. 作为普通函数,直接调用。浏览器中,this 为 window(即给 window 添加属性和方法)
- c. 在另一个对象的作用域中调用
var o = new Object()
Person.call(o, 'Jack', 24, 'Nurse')
o.sayName() //"Jack"
构造函数的问题
每个方法都要在每个实例上重新创建一遍,即构造出的不同对象 person1、person2,其同名方法(sayName)并不是同一个 Function 实例
this.sayName = new Function('console.log(this.name)') //每个person实例都包含一个不同Function实例
实际并不需要在执行代码前就把函数绑定到特定对象上面。解决:将函数定义转移到构造函数外部
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName() {
alert(this.name)
}
var person1 = new Person('Nicholas', 29, 'Doctor')
var person2 = new Person('Nicholas', 27, 'Nurse')
alert(person1.sayName == person2.sayName) //true
【注】person1 和 person2 对象共享了在全局作用域定义的同一个 sayName() 函数;但是这样一来,自定义引用类型就失去了封装性。
6.2.3 原型模式
prototype:通过构造函数创建的那个对象实例的原型对象 每个函数都有一个 prototype 指针属性,指向一个包含由特定类型的所有实例共享的属性和方法 使用原型对象的好处:让所有对象实例共享它所包含的属性和方法(不必在构造函数中定义对象实例的信息)
function Person(name, age, job) {
Person.prototype.name = 'Nicholas' //将属性赋给this对象
Person.prototype.age = 24
Person.prototype.job = 'Doctor'
Person.prototype.sayName = function () {
console.log(this.name)
}
}
var person1 = new Person()
person1.sayName() //Nicholas
var person2 = new Person()
person2.sayName() //Nicholas
alert(person1.sayName == person2.sayName) //true
【注】此时构造函数变成了空函数,person1 和 person2 访问的都是同一组属性和同一个 sayName 函数
1. 理解原型对象
每个函数都有一个 prototype 属性,它指向函数的原型对象。 默认情况下,所有原型对象都会自动获得一个 constructor 属性,其指向 prototype 属性所在函数的指针。前述 Person.prototype.constructor 指向 Person 函数。 每个构造函数创建的新实例都包含一个[[Prototype]]指针属性(浏览器中为proto),指向构造函数的原型对象。指针连接存在于实例与构造函数的原型对象之间(实例对象与构造函数没有直接关系)
person1
Person {}
__proto__:
age:24
job:"Doctor"
name:"Nicholas"
sayName:ƒ ()
constructor:ƒ Person(name, age, job)
__proto__:Object
Person
ƒ Person(name, age, job) { Person.prototype.name = 'Nicholas'; //将属性赋给this对象 Person.prototype.age = 24;
Person.prototype.job = 'Doctor'; Person.prototype.sayName = function() { consol…
Person.prototype
{name: "Nicholas", age: 24, job: "Doctor", sayName: ƒ, constructor: ƒ}
age:24
job:"Doctor"
name:"Nicholas"
sayName:ƒ ()
constructor:ƒ Person(name, age, job)
__proto__:Object
确定实例对象与原型对象之间的关系:(通过实例对象获取[[Prototype]]指针指向的原型对象)
- 1、[[Prototype]]在浏览器中支持的proto属性
- 2、原型对象的 isPrototypeOf(obj) 方法(若参数的[[Prototype]]指向调用该方法的对象,则返回 true)
- 3、Object.getPrototypeOf(obj)
Person.prototype == person1.__proto__ //true
Person.prototype.isPrototypeOf(person1) //true
Object.getPrototypeOf(person1) == Person.prototype //true
对象实例共享原型属性和方法的基本原理:
调用 person1.sayName()时,会先后执行两次搜索。首先解析器搜索实例 person1 是否有 sayName 属性,结果是没有;然后搜索 person1 的原型是否有 sayName 属性,结果有;于是读取保存在原型对象中的函数。
重定义原型属性:
可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。 在实例中添加的属性与该原型中的属性同名时,该实例属性会被创建,并屏蔽原型中的同名属性。
var person1 = new Person()
var person2 = new Person()
person1.name = 'Greg'
alert(person1.name) //"Greg" ——来自实例
alert(person2.name) //"Nicholas" ——来自原型
使用 delete 删除实例属性:
var person1 = new Person()
person1.name = 'Greg'
alert(person1.name) //Greg
delete person1.name
alert(person1.name) //Nicholas
hasOwnProperty(prop)方法检测该属性是否在实例中: 便于知道访问的是实例属性还是原型属性
var person1 = new Person()
alert(person1.hasOwnProperty('name')) //false
person1.name = 'Greg'
alert(person1.hasOwnProperty('name')) //true
delete person1.name
alert(person1.hasOwnProperty('name')) //false
2、原型与 in 操作符
in 操作符的两种使用场景:单独使用、for-in 循环
(1)单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中
var person1 = new Person()
alert('name' in person1) //true
person1.name = 'Greg'
alert('name' in person1) //true
(2)使用 for-in 循环时,返回是是所有能通过对象访问的、可枚举的属性(既包括实例中的属性,也包括原型中的属性)
var person1 = new Person() //实例无属性
for (var prop in person1) {
console.log(prop)
}
//name
//age
//job
//sayName
Object.keys(obj) 取得对象所有可枚举属性 //return [props] 参数为原型对象时,返回原型对象属性;参数为实例对象时,返回实例对象属性。
Object.keys(Person.prototype) //["name", "age", "job", "sayName"]
var person1 = new Person()
person1.name = 'Rob'
person1.age = 24
Object.keys(person1) //["name", "age"]
Object.getOwnPropertyNames(obj) 获取所有实例属性(无论是否可枚举)
var person1 = new Person()
Object.getOwnPropertyNames(person1) //[]
Object.getOwnPropertyNames(Person.prototype) //["constructor", "name", "age", "job", "sayName"]
【注】constructor 为不可枚举属性。Object.keys(person1), Object.getOwnPropertyNames(person1) 都可以替代 for-in 循环
3、更简单的原型语法
用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person() {}
Person.prototype = {
name: 'Nicholas',
age: 25,
sayName: function () {
console.log(this.name)
}
}
Person.prototype.constructor //ƒ Object() { [native code] }
new Person().constructor == Person //false
new Person() instanceof Person //true
【注】:constructor 属性不在指向 Person。当创建一个函数时,同时会创建其 prototype 对象,该对象自动获得 constructor 属性。而这里本质上是重写了默认的 prototype 对象,因此 constructor 属性指向 Object 构造函数。
设置恢复 constructor:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 25,
sayName: function () {
console.log(this.name)
}
}
Person.prototype.constructor //ƒ Person() {}
new Person().constructor == Person //true
Person.prototype //{constructor: ƒ, name: "Nicholas", age: 25, sayName: ƒ}
【注】上述添加 constructor 属性,使得 constructor 的 [[Enumerable]] 特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的
Object.defineProperty(obj, prop, {descriptor}) 重设构造函数
function Person() {}
Person.prototype = {
name: 'Nicholas',
age: 25,
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'consructor', {
enumerable: false,
value: Person
})
Person.prototype //{name: "Nicholas", age: 25, sayName: ƒ, constructor: ƒ}
4、原型的动态性
由于在原型中查找值是一次搜索,因此对原型所做的修改都可反映出来。允许先创建实例,再为原型对象添加方法,然后实例调用该方法
var person1 = new Person()
Person.prototype.sayHi = function () {
alert('Hi')
}
person1.sayHi() //Hi
但是当重写整个原型对象时,等于切断了构造函数与最初原型之间的联系 [[Prototype]] (proto) 实例对象的proto属性引用的仍是重写前的原型
function Person() {}
var person1 = new Person()
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 25,
sayName: function () {
console.log(this.name)
}
}
person1.sayName() //Uncaught TypeError: person1.sayName is not a function
person1.__proto__ //{constructor: ƒ}
5、原生对象的原型
所有原生引用类型都在其构造函数上定义了方法:
typeof Array.prototype.sort //function
typeof String.prototype.substring //function
通过原生对象的原型,还可以定义新方法:
例:为 String 包装类型添加 startsWith() 方法
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0
}
'Hello world'.startsWith('Hello') //true
【注】不推荐产品化的程序中修改原生对象的原型
6、原型对象的问题
原型模式的最大问题是由其共享的本性导致的:
对于包含引用类型的属性来说,实例 1 对原型引用类型属性的修改会同时表现在其他实例中(而无法有属于自己的属性)——所以一般很少单独使用
function Person() {}
var person1 = new Person()
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 25,
friends: ['Jack', 'Rob'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('Van')
alert(person1.friends) //Jack,Rob,Van
alert(person2.friends) //Jack,Rob,Van
6.2.4 组合使用构造函数和原型模式(广泛使用)
构造函数用于定义实例属性(支持传参),原型模式用于定义方法和共享属性
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Jack', 'Rob']
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name)
}
}
var person1 = new Person('Nicholas', 25, 'Software Engineer')
var person2 = new Person('Greg', 22, 'Doctor')
person1.friends.push('Van')
alert(person1.friends) //Jack,Rob,Van
alert(person2.friends) //Jack,Rob
alert(person1.sayName === person2.sayName) //true
6.2.5 动态原型模式
把所有信息封装在构造函数中,通过在构造函数中初始化原型,检查某个应该存在的方法是否有效,来决定是否需要初始化原型方法
function Person(name, age, job) {
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != 'Function') {
Person.prototype.sayName = function() {
alert(this.name)
}
}
var person1 = new Person('Greg', 28, 'Doctor')
person1.sayName() //Greg
【注】只在 sayName()方法不存在情况下才会将它加到原型中。首次调用构造函数会执行 if 中的原型方法初始化语句,此后不需要在修改了。
使用动态原型模式时,不能使用对象字面量重写原型。(避免在已经创建了实例情况下重写原型,会切断现有实例与新原型之间的联系)
6.2.6 寄生构造函数模式
创建的函数仅仅是包装对象,然后返回新创建的对象,使用 new 操作符实例化(仅这一点区别于工厂模式)
function Person(name, age, job) {
var o = new Object() //显式创建对象
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o //返回对象
}
var person1 = new Person('Greg', 27, 'Doctor')
person1.sayName() //Greg
【注】返回对象与构造函数(原型)之间没有关系,函数返回的对象与在构造函数外部创建的对象没什么不同,所以不能依赖 instanceof 操作符确定对象类型
6.2.7 稳妥构造函数模式
稳妥对象(durable objects):没有公共属性,新创建对象的实例方法不引用 this,不使用 new 操作符调用构造函数
function Person(name, age, job) {
var o = new Object() //显式创建对象
//可定义私有变量和函数
//公共方法
o.sayName = function () {
console.log(name)
}
return o //返回对象
}
var person1 = Person('Greg', 27, 'Doctor')
person1.sayName() //Greg
【注】除了调用 sayName()方法外,没有别的方法可以访问其数据成员,提供了安全性
6.3 继承
面向对象语言支持:接口继承(继承方法签名)、实现继承(继承实际方法)
ECMAScript 只支持实现继承,依靠原型链来实现
6.3.1 原型链
原型链实现继承的思想:利用原型让一个引用类型继承另一个引用类型的属性和方法 让子类原型对象等于父类的实例,原型对象将包含一个指向另一个原型的指针; 继承:通过创建父类的实例,并将该实例赋给子类的原型实现。本质:重写子类原型对象,代之以一个父类的实例。即原来存在于父类的实例中的所有属性和方法,现在也存在于子类原型对象中了。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
//继承了SuperType,子类原型包括父类实例属性和指向父类原型对象的指针
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
return this.subproperty
}
var instance = new SubType()
alert(instance.getSuperValue()) //true
SuperType.prototype
{getSuperValue: ƒ, constructor: ƒ}
SubType.prototype
SuperType {property: true, getSubValue: ƒ}
getSubValue:ƒ ()
property:true
__proto__:
getSuperValue:ƒ ()
constructor:ƒ SuperType()
__proto__:Object
SubType.prototype.__proto__
{getSuperValue: ƒ, constructor: ƒ}
SubType.prototype.constructor
ƒ SuperType() {
this.property = true
}
instance.__proto__
SuperType {property: true, getSubValue: ƒ}
instance.constructor
ƒ SuperType() {
this.property = true
}
【注】a. 子类原型不仅具有父类实例所拥有的全部属性和方法,还有一个指针,指向父类的原型。 b.子类实例的 constructor 属性等于子类原型的 constructor 属性。而子类原型又指向父类原型,而父类原型有 constructor 属性,于是子类实例的 constructor 属性等于父类原型的 constructor 属性。
1、别忘记默认的原型
所有引用类型默认都继承了 Object,所有函数的默认原型都是 Object 的实例,因此默认原型都会有一个内部指针,指向 Object.prototype。(所有自定义类型都会继承 toString()、valueOf()等默认方法)完整的原型链如图:
2、确定原型和实例的关系
(1)instanceof 操作符测试实例与原型链中出现过的构造函数
alert(instance instanceof Object) //true
alert(instance instanceof SuperType) //true
alert(instance instanceof SubType) //true
2)isPrototypeof() 方法,只要是原型链中出现过的原型,都可以说是该原型链所派生出的实例的原型
alert(Object.prototype.isPrototypeOf(instance)) //true
alert(SuperType.prototype.isPrototypeOf(instance)) //true
alert(SubType.prototype.isPrototypeOf(instance)) //true
3、先替换原型再添方法
子类覆盖(重定义)父类方法或添加父类没有的方法,需要在替换原型之后进行。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
//继承了SuperType
SubType.prototype = new SuperType()
//添加新方法
SubType.prototype.getSubValue = function () {
return this.subproperty
}
//重写父类的方法
SubType.prototype.getSuperValue = function () {
return false
}
var instance = new SubType()
alert(instance.getSuperValue()) //false
【注】在通过原型链实现继承时,不能使用对象字面量(Object 实例)创建原型方法(会重写原型链)
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
//使用对象字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
getSubType: function () {
return this.subproperty
}
}
var instance = new SubType()
alert(instance.getSuperValue()) //Uncaught TypeError: instance.getSuperValue is not a function
4、原型链的问题
(1)最主要问题来自包含引用类型的原型:在构造函数中定义引用类型属性,并通过原型实现继承,父类实例的引用类型属性变成了子类原型的引用类型属性。相当于为子类原型创建引用类型属性一样:SubType.prototype.colors = [xxx],所有子类实例都会共享这一引用属性。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {}
//继承了SuperType
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) //red,blue,green,black
var instance2 = new SubType()
alert(instance2.colors) //red,blue,green,black
(2)在创建子类实例时,不能向父类构造函数中传递参数(实践中很少单独使用原型链)
6.3.2 借用构造函数
借用构造函数(constructor stealing)(伪造对象或经典继承):在子类构造函数的内部使用父类构造函数。
函数是:在特定环境中执行代码的对象
1、向父类构造函数传递参数
function SuperType(name) {
this.name = name
}
function SubType() {
//继承了SuperType,同时传递参数
SuperType.call(this, 'Nicholas')
//实例属性
this.age = 29
}
var instance = new SubType()
console.log(instance.name, instance.age) //Nicholas 29
【注】为避免父类构造函数重写子类属性,可在调用父类构造函数后,添加子类自己的属性
2、借用构造函数的问题
方法都在构造函数中定义,函数复用无从谈起;父类原型中的方法对子类不可见。(?)
6.3.3 组合继承(combination inheritance)——最常用继承
又称伪经典继承:将原型链和借用构造函数组合到一块
思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
//继承属性
SuperType.call(this, name)
//实例属性
this.age = age
}
//继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType //重写子类构造函数以便实例化传参
SubType.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() //Nicholas
instance1.sayAge() //29
var instance2 = new SubType('Greg', 27)
console.log(instance2.colors) //["red", "blue", "green"]
instance2.sayName() //Greg
instance2.sayAge() //27
【注】组合继承最大问题:必须调用两次父类构造函数,一次是创建子类原型时,另一次是在子类构造函数内部。 即子类最终包含父类对象的全部实例属性,我们不得不在调用子类构造函数时重写这些属性。
instance1
SubType {name: "Nicholas", colors: Array(4), age: 29}
age:29
colors:(4) ["red", "blue", "green", "black"]
name:"Nicholas"
__proto__:SuperType
colors:(3) ["red", "blue", "green"]
constructor:ƒ SubType(name, age)
name:undefined
sayAge:ƒ ()
__proto__:Object
6.3.4 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
本质:object() 对传入其中的对象执行一次浅复制
原型式继承要求必须有一个对象可以作为新对象的基础(原型),原型属性会被其他新对象所共享(相当于创建了初始对象的副本)
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = object(person)
person1.name = 'Greg'
person1.friends.push('Rob')
var person2 = object(person)
person2.name = 'Linda'
person2.friends.push('Jack')
alert(person.friends) //Shelby,Court,Van,Rob,Jack
ECMAScript 5 新增了 Object.create(protoObj, [propObj]) 方法规范了原型式继承 方法接收两参数:用作新对象原型的对象、为新对象定义额外属性的对象(可选) 传入一个参数时,Object.create(protoObj) 等于 Object(protoObj)
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = Object.create(person)
person1.name = 'Greg'
person1.friends.push('Rob')
var person2 = Object(person)
person2.name = 'Linda'
person2.friends.push('Jack')
alert(person.friends) //Shelby,Court,Van,Rob,Jack
Object.create() 方法的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都通过自己的描述符定义 (会覆盖原型对象上的同名属性)
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = Object.create(person, {
name: {
value: 'Greg'
}
})
alert(person1.name) //Greg
6.3.5 寄生式继承
寄生式继承与原型式继承紧密相关,与寄生构造函数和工厂模式相似:即创建一个仅用于封装继承过程的函数,函数内部增强对象,最后返回对象
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
function createAnother(original) {
var clone = object(original) //调用构造函数创建新对象
clone.sayHi = function () {
//增强对象
alert('hi')
}
return clone
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
}
var person1 = createAnother(person)
person1.sayHi() //hi
【注】使用寄生式继承为对象添加函数,不能做到函数复用,与构造函数模式类似
6.3.6 寄生组合式继承(最理想的继承范式)
组合继承最大问题:必须调用两次父类构造函数,一次是创建子类原型时(子类原型得到两属性),另一次是在子类构造函数内部(在新对象上创建两实例属性)。新对象创建的实例属性屏蔽了原型中的两同名属性。
instance1
SubType {name: "Nicholas", colors: Array(4), age: 29}
age:29
colors:(4) ["red", "blue", "green", "black"]
name:"Nicholas"
__proto__:SuperType
colors:(3) ["red", "blue", "green"]
constructor:ƒ SubType(name, age)
name:undefined
sayAge:ƒ ()
__proto__:Object
寄生式组合继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类的原型而调用父类的构造函数(避免了父类构造实例属性)。本质上,就是使用寄生式继承父类原型,再将结果指定给子类原型
function object(o) {
function F() {} //临时构造函数
F.prototype = o //将传入的原型对象作为此构造函数的原型
return new F() //返回临时构造函数新实例
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype) //创建子类原型对象,其原型为父类原型
prototype.constructor = subType //增强原型对象
subType.prototype = prototype //指定原型对象
}
改写前面组合继承的例子:
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) //调用一次父类构造函数
this.age = age
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function () {
alert(this.age)
}
var instance1 = new SubType('Jack', 33)
instance1
SubType {name: "Jack", colors: Array(3), age: 33}
age:33
colors:(3) ["red", "blue", "green"]
name:"Jack"
__proto__:SuperType
constructor:ƒ SubType(name, age)
sayAge:ƒ ()
__proto__:
sayName:ƒ ()
constructor:ƒ SuperType(name)
__proto__:Object
6.4 小结
创建对象的模式
- 工厂模式:使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。被构造函数模式所取代。
- 构造函数模式:可以使用 new 操作符,但其成员无法复用(包括函数)
- 原型模式:使用构造函数的 prototype 属性来指定哪些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,使用原型定义共享的属性和方法。
继承
- 原型链继承:通过将一个父类实例赋给子类原型实现,子类就能访问父类的所有属性和方法。 原型链的问题:对象实例共享所有继承的属性和方法(不宜单独使用) 解决原型链问题:借用构造函数。即在子类构造函数内部调用父类构造函数(call),每个实例都具有自己的属性,只使用构造函数模式来定义类型。组合继承用的最多,其使用原型链继承共享的属性和方法,通过借用构造函数继承实例属性。
- 原型式继承:在不必预先定义构造函数情况下实现继承,本质是执行对给定对象的浅复制 Object.create()。而复制得到的副本可再改造。
- 寄生式继承:与原型式继承相似,结合了原型式继承和工厂模式,但对象函数无法复用。
- 寄生组合式继承:集寄生式继承和组合继承的优点,最有效。
第 7 章 函数表达式
定义函数的方式有两种:
- 函数声明: 重要的是函数声明提升,函数表达式不会提升。
funName()
function funName() {}
这个例子中,funName() 可以正常调用,不会报错。因为在代码执行之前,解析器会率先读取函数声明,并使其在执行任何代码之前可用。
- 函数表达式:
funName()
var funName = function () {}
上面这个例子中,funName() 会报错,因为解析器会率先读取函数表达式,发现这是一个变量声明,因此会在任何代码执行之前就尝试获取 funName 的值。但这时候 funName 还没有被赋值,因此会导致错误。
7.1 递归
递归函数是在一个函数通过名字调用自身的情况下构成的,如下所示:
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * factorial(num - 1)
}
}
这是一个经典的递归函数,它执行了 num 和 factorial(num-1) 之间的乘法操作,而 factorial(num-1) 又执行了 num 和 factorial(num-2) 之间的乘法操作,以此类推,直到 num 等于 1。
7.2 闭包
有不少开发人员总是搞不清匿名函数和闭包之间的区别。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
function createComparisonFunction(propertyName) {
return function (object1, object2) {
var value1 = object1[propertyName]
var value2 = object2[propertyName]
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
}
在这个例子中,我们创建了一个函数,该函数接受一个参数(propertyName),然后返回一个函数。返回的函数在执行时,会根据给定的对象(object1 和 object2)的 propertyName 属性值进行比较。 使用闭包的场景:
- 封装变量:保护变量不被外部访问
- 实现公有变量:可以通过闭包来创建可以访问私有变量的公有方法
闭包的优缺点 优点:
- ① 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
- ② 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
- ③ 匿名自执行函数可以减少内存消耗
缺点:
- ① 其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为 null;
- ② 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。
下面是一个闭包,不会在函数调用结束后被垃圾回收机制回收的例子:
function assignHandler() {
var element = document.getElementById('someElement')
element.onclick = function () {
alert(element.id)
}
}
使用闭包注意点:
- 闭包会增加内存消耗,因为闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。 以上示例优化如下:
function assignHandler() {
var element = document.getElementById('someElement')
element.onclick = function () {
alert(element.id)
}
element = null
}
7.3 模仿块级作用域
匿名函数可以用来模仿块级作用域,因为块级作用域可以避免变量污染。
;(function () {
// 这里是块级作用域
})()
7.4 私有变量
私有变量包括函数的局部变量和对象的私有属性。
function Person(name) {
// 私有变量
var privateVariable = 'private'
// 私有函数
function privateFunction() {
console.log('privateFunction')
}
// 公有属性和方法
this.name = name
this.sayName = function () {
console.log(this.name)
}
}
7.4.1 静态私有变量
通过在私有作用域中定义私有变量和函数,同样也可以创建特权方法,特权方法是指有权访问私有变量和私有函数的公有方法。
function MyObject() {
// 私有变量
var privateVariable = 'private'
// 私有函数
function privateFunction() {
console.log('privateFunction')
}
// 特权方法
this.publicMethod = function () {
privateVariable = 'public'
privateFunction()
}
}
7.4.2 模块模式
模块模式是为单例创建私有变量和特权方法。
var singleton = (function () {
// 私有变量
var privateVariable = 'private'
// 私有函数
function privateFunction() {
console.log('privateFunction')
}
// 返回公有方法
return {
publicMethod: function () {
privateVariable = 'public'
privateFunction()
}
}
})()
这个例子中,我们创建了一个名为 singleton 的对象,它包含一个公有方法 publicMethod,该方法可以访问私有变量 privateVariable 和私有函数 privateFunction。
7.4.3 增强的模块模式
增强的模块模式是在增强的模块模式基础上,创建一个自定义的对象。增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加额外的属性和方法。
var myNamespace = (function (my) {
var privateVariable = 'private'
function privateFunction() {
console.log('privateFunction')
}
my.publicMethod = function () {
privateVariable = 'public'
privateFunction()
}
return my
})(myNamespace || {})
7.5 小结
匿名函数又称为拉姆达函数,它没有函数名,也没有函数声明。匿名函数通常被用作回调函数,或者作为立即执行函数表达式(IIFE)。有以下特点
- 函数表达式不同于函数声明,函数声明要求有一个函数名,而函数表达式则可以省略函数名。
- 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂
- 递归函数应该始终有一个退出条件,否则会导致无限递归
- 在后台执行环境中,闭包的作用域链包含着它自己的作用域,函数的作用域以及全局作用域
- 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁
- 函数返回了一个闭包,这个函数的作用域会在内存中保存到闭包不存在为止
- 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下该函数的引用,结果就是函数内部的所有变量都会被立即销毁--除非某些变量赋值给了包含作用域(即外部作用域)中的变量
闭包还可以用于对象中创建私有变量,相关概念和要点如下:
- 即使 Javascript 中没有私有变量的概念,但可以利用闭包来实现私有变量,通过访问闭包可以访问函数内部的变量,从而实现私有变量的概念。
- 闭包可以用于创建私有变量和特权方法,特权方法是指有权访问私有变量和私有函数的公有方法。
- 模块模式是为单例创建私有变量和特权方法。
- 增强的模块模式是在增强的模块模式基础上,创建一个自定义的对象。增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加额外的属性和方法。
- 可以使用函数构造函数来创建自定义对象,从而实现私有变量和特权方法。
第 8 章 BOM
BOM 是浏览器对象模型(Browser Object Model)的缩写,它提供了独立于内容和浏览器窗口的对象,用于访问和操作浏览器的窗口和文档。
8.1 window 对象
BOM 的核心对象是 window,它表示浏览器的一个实例。在浏览器中,window 对象有双重角色:它既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象。
8.1.1 全局作用域
由于 JavaScript 是 ECMAScript 的实现,所以所有全局变量和函数都是作为 window 对象的属性和方法创建的。
var a = 2
function b() {}
console.log(window.a) // 2
console.log(window.b) // function b() {}
8.1.2 窗口关系及框架
如果在一个 HTML 页面中包含多个框架,那么每个框架都有自己的 window 对象,而且它们之间没有关系。
<iframe src="URL_ADDRESS" name="example"></iframe>
8.1.3 窗口位置
用来确定和修改 window 对象位置的属性和方法有很多。IE、Safari、Opera 和 Chrome 都提供了 scrollLeft、scrollTop 属性分别表示窗口相对于屏幕左边和上边的位置。Firefox 则在 scrrenX 和 screenY 属性中提供了同样的信息。Safari、Chrome 同时支持这两个属性。Opera 虽然支持 screenX 和 screenY 属性,但与 scrollLeft 和 scrollTop 属性并不对应,因此建议不要在 Opera 中使用这些属性。使用下列代码兼容跨浏览器取得窗口左边和上边的位置。
var leftPos =
typeof window.screenLeft == 'number' ? window.screenLeft : window.screenX
var topPos =
typeof window.screenTop == 'number' ? window.screenTop : window.screenY
这个例子中,我们首先检查 window.screenLeft 属性是否是一个数字,如果是,就返回它的值,否则返回 window.screenX 属性的值。然后,我们检查 window.screenTop 属性是否是一个数字,如果是,就返回它的值,否则返回 window.screenY 属性的值。
8.1.4 窗口大小
跨浏览器获得窗口大小不是一件容易的事,因为每个浏览器都有自己的实现方式。IE9、Safari、Opera 和 Chrome 都提供了 4 个属性:innerWidth、innerHeight、outerWidth 和 outerHeight。
在 IE9+、Safari 和 Firefox 中,outerWidth 和 outerHeight 属性返回浏览器窗口本身的尺寸,而 innerWidth 和 innerHeight 属性返回页面视口的尺寸。
在 Opera 中,outerWidth 和 outerHeight 属性表示页面视图容器的大小,而 innerWidth 和 innerHeight 表示该容器中页面视图区的大小(减去边框宽度)
在 Chrome 中,outerWidth、outerHeight 与 innerWidth 、 innerHeight 返回的是相同的值。即视口大小而非浏览器窗口本身的尺寸。
IE8 及更早版本的浏览器中,没有提供 innerWidth 和 innerHeight 属性,因此需要使用 document.documentElement.clientWidth 和 document.documentElement.clientHeight 属性来获取视口大小。
在 IE、Firefox、Safari、Opera 和 Chrome 中,document.documentElement.clientWidth 和 document.documentElement.clientHeight 保存了页面视口的信息。
在 IE6 中,这些属性必须在标准模式下才能使用,因此需要在文档中包含一个 doctype 声明。如果是混杂模式,就必须通过 document.body.clientWidth 和 document.body.clientHeight 来取得相同的信息。
而对于混杂模式的 Chrome,document.documentElement.clientWidth 和 document.documentElement.clientHeight 返回的是 0,因此需要使用 document.body.clientWidth 和 document.body.clientHeight。
虽然最终无法确定浏览器窗口本身的大小,但是却可以取得页面视口的大小。如下所示:
var 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.innerWidth 属性是否是一个数字,如果是,就返回它的值,否则返回 document.documentElement.clientWidth 属性的值。然后,我们检查 window.innerHeight 属性是否是一个数字,如果是,就返回它的值,否则返回 document.documentElement.clientHeight 属性的值。
对于移动设备,window.innerWidth 和 window.innerHeight 保存着可见视口,也就是屏幕上可见页面的区域大小。移动 IE 浏览器不支持这两个属性,因此需要使用 document.documentElement.clientWidth 和 document.documentElement.clientHeight 属性来获取视口大小。
在其他移动浏览器中,document.documentElement 度量的是布局视口,即渲染后页面的实际大小(与可见视口不同,可见视口是实际页面大小的一小部分)。移动 IE 浏览器把布局信息保存在 document.body.clientWidth 和 document.body.clientHeight 中。这些值不会随着页面缩放而变化。
由于与桌面浏览器存在这些差异,最好先检测一下用户是否在使用移动浏览器,再决定使用哪个属性 下面是兼容性代码:
// 检测一下用户是否在使用移动浏览器
var isMobile = {
Android: function () {
return navigator.userAgent.match(/Android/i)
},
BlackBerry: function () {
return navigator.userAgent.match(/BlackBerry/i)
},
iOS: function () {
return navigator.userAgent.match(/iPhone|iPad|iPod/i)
},
Opera: function () {
return navigator.userAgent.match(/Opera Mini/i)
},
Windows: function () {
return navigator.userAgent.match(/IEMobile/i)
},
any: function () {
return (
isMobile.Android() ||
isMobile.BlackBerry() ||
isMobile.iOS() ||
isMobile.Opera() ||
isMobile.Windows()
)
}
}
var pageWidth = window.innerWidth,
pageHeight = window.innerHeight
// 判断是移动 IE 浏览器 需要使用 document.documentElement.clientWidth 和 document.documentElement.clientHeight 属性来获取视口大小
if (isMobile.Windows()) {
pageWidth = document.documentElement.clientWidth
pageHeight = document.documentElement.clientHeight
} else 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
}
}
另外,resizeTo 和 resizeBy 方法可以调整浏览器窗口的大小。这两个方法都接受两个参数:新的宽度和新的高度。这两个方法都只能在浏览器窗口中使用,不能在框架中使用。resizeTo 方法会立即调整浏览器窗口的大小,而 resizeBy 方法会调整浏览器窗口的大小,使浏览器窗口的宽度和高度分别增加或减少指定的像素数。
window.resizeTo(100, 100) //调整窗口大小为100x100
window.resizeBy(100, 100) //调整窗口大小为200x200
moveTo 和 moveBy 方法可以移动浏览器窗口的位置。这两个方法都接受两个参数:新的 x 坐标和新的 y 坐标。这两个方法都只能在浏览器窗口中使用,不能在框架中使用。moveTo 方法会立即移动浏览器窗口的位置,而 moveBy 方法会移动浏览器窗口的位置,使浏览器窗口的 x 坐标和 y 坐标分别增加或减少指定的像素数。
window.moveTo(100, 100) //移动窗口位置为100,100
window.moveBy(100, 100) //移动窗口位置为200,200
8.1.5 导航和打开窗口
1.弹出窗口
window.open() 方法可以打开一个新的浏览器窗口。这个方法接受三个参数:要加载的 URL、窗口目标和一个特性字符串。
window.open('URL_ADDRESS', '_blank', 'width=100,height=100')
对于浏览器的主窗口,没有用户的允许是不能关闭它的。弹出窗口可以通过调用 top.close()方法在不经过用户同意的情况下关闭自己。弹出窗口关闭后,窗口的引用仍然存在,除了检测其 closed 属性外,没有其他的作用。例如:
var popup = window.open('URL_ADDRESS', '_blank', 'width=100,height=100')
popup.close()
alert(popup.closed) // true
新创建的 window 对象有一个 opener 属性,其中保存打开它的原始窗口对象。这个属性只在弹出窗口的最外层 window 对象(top)中有定义,而且指向调用 window.open 方法的窗口或框架。例如:
var popup = window.open('URL_ADDRESS', '_blank', 'width=100,height=100')
alert(popup.opener === window) // true
popup.opener = null
将 opener 属性设置为 null 就是告诉浏览器新创建的标签页不需要与打开它的标签页通信,因此可以在独立的进程中运行,从而提高了安全性。标签页之间联系一旦断开,将没有办法恢复。
2.安全限制
浏览器对弹出窗口配置方面添加了限制。
弹出窗口不能访问 opener 窗口的私有变量和函数。
IE6 不允许在屏幕以外的位置创建窗口,不允许将弹框窗口的位置移动到屏幕以外,不允许关闭状态栏。IE7 不允许关闭地址栏,不允许移动弹框窗口位置及调整大小。
Firefox1 从一开始就不允许修改地址栏,因此无论给 window.open 传递什么样的特性字符串,弹框的窗口也会显示状态栏。Firefox3 强制始终在弹出的窗口中显示地址栏。
Opera 只允许在主浏览器窗口中打开弹出窗口。但不允许他们出现在与系统弹框窗口重叠的区域。
有些浏览器只能通过用户单击“打开”按钮才能打开弹出窗口。
对于那些用户无意打开的弹框,Chrome 不会像其他浏览器屏蔽弹框,而是只显示他们的标题栏,并把他们放在浏览器的右下角。
3.弹出窗口屏蔽程序
大多数浏览器都内置弹出窗口屏蔽程序。如果屏蔽程序检测到弹出窗口,就会阻止它的显示。
弹出窗口被屏蔽有两种可能:
- 如果是浏览器内置的屏蔽程序阻止的弹出窗口,那么 window.open()返回的是 null。例如:
var popup = window.open('URL_ADDRESS', '_blank', 'width=100,height=100')
if (popup === null) {
alert('弹出窗口被屏蔽')
}
- 如果是浏览器扩展或者其他程序阻止的弹出窗口,那么 window.open()通常会抛出一个错误。例如:
var block = false
try {
var popup = window.open('URL_ADDRESS', '_blank', 'width=100,height=100')
if (popup === null) {
block = true
}
} catch (e) {
block = true
}
if (block) {
alert('弹出窗口被屏蔽')
}
在任何情况,以上代码都可以检测出调用 window.open()方法打开的窗口是否被屏蔽。但是需要注意,检测弹出窗口是否被屏幕是一回事,它并不会阻止浏览器显示与被屏蔽窗口的有关信息。
8.1.6 间歇调用和超时调用
- 间歇调用 setInterval(),调用方式:
var intervalId = setInterval(function () {
// 要执行的代码
}, 1000)
// 停止间歇调用
clearInterval(intervalId)
- 超时调用 setTimeout(),调用方式:
var timeoutId = setTimeout(function () {
// 要执行的代码
}, 1000)
// 停止超时调用
clearTimeout(timeoutId)
8.1.7 系统对话框
浏览器通过 alert、confirm、prompt 等方法来显示系统对话框。这些方法都接受一个字符串作为参数,这个字符串就是对话框中显示的文本。
- alert() 方法 参数:要显示的文本 无返回值 显示一个带有一个按钮的对话框,点击按钮后对话框会消失。
alert('Hello World')
- confirm() 方法 参数:要显示的文本 返回值:true 或 false 显示一个带有两个按钮的对话框,点击按钮后对话框会消失。例如:
if (confirm('您是否要进入?')) {
// 进入
}
- prompt(?,?):用于显示可提示用户进行输入的对话框
- 第一个问是显示的文本,
- 第二个问是输入框中的默认值(可不写)
- 点击确认返回输入框中的内容,取消返回 null
var name = prompt('请输入您的姓名', '张三')
if (name === null) {
alert('取消了')
} else {
alert('您的姓名是:' + name)
}
- 查找对话框 window.find(),打印对话框 window.print() 这两个方法不会就用户操作给出任何信息,
8.2 location 对象
location 对象是 BOM 的一个部分,它既是 window 对象的属性,也是 document 对象的属性。它提供了与当前窗口加载的文档的有关信息,还提供了一些导航功能。
属性名 | 例子 | 说明 |
---|---|---|
hash | "#contents" | 返回 URl 中的 hash(#跟零或多个字符串),如果字符串不包含散列,则返回空字符串 |
host | "www.wrox.com:80" | 返回服务名称和端口号(如果有,http 默认 8080,https 默认 443) |
hostname | "www.wrox.com" | 返回不带端口号的服务器名称 |
href | "http://www.wrox.com" | 返回当前加载页面的完整 URL。location 对象的 toString()方法也返回这个值 |
pathname | "/books/dom3/" | 返回 URL 中的路径部分 |
port | "8080" | 返回 url 中指定的端口号,如果没有指定端口号,则返回空字符串 |
protocol | "http:" | 返回页面使用的协议。通常是 http 或 https |
search | "?q=javascript" | 返回 url 的查询字符串。这个字符串以?开头 |
8.2.1 查询字符串
解析 url 的查询字符串。示例如下
function getQueryStringArgs() {
// 取得查询字符串并去掉开头的问号
var qs = location.search.length > 0 ? location.search.substring(1) : ''
// 保存数据的对象
var args = {}
// 取得每一项
var items = qs.length ? qs.split('&') : []
var item = null
var name = null
var value = null
// 逐个将每一项添加到 args 对象中
for (var i = 0; i < items.length; i++) {
item = items[i].split('=')
name = decodeURIComponent(item[0])
value = decodeURIComponent(item[1])
if (name.length) {
args[name] = value
}
}
return args
}
8.2.2 位置操作
改变浏览器的位置。示例如下:
// 导航到新的页面并追加历史记录
location.assign('URL_ADDRESS')
window.location.href = 'URL_ADDRESS'
window.location = 'URL_ADDRESS'
通过 search,hash,pathname,port,protocol,host,hostname 可以改变当前 url。示例如下:
// 改变当前 url
location.search = '?q=javascript'
location.hash = '#contents'
location.pathname = '/books/dom3/'
location.port = '8080'
location.protocol = 'http:'
location.host = 'www.wrox.com:80'
location.hostname = 'www.wrox.com'
调用 replace()方法可以在不添加历史记录的情况下导航到新的页面。不能返回前一个页面,示例如下:
location.replace('URL_ADDRESS')
8.3 navigator 对象
每一个浏览器的 navigator 对象都有自己的一套属性。下表列出了存在于所有浏览器的属性和方法,以及支持他们的浏览器版本。
第 9 章 客户端检测
第 10 章 DOM
第 11 章 DOM 扩展
第 12 章 DOM2 和 DOM3
第 13 章 事件
第 14 章 表单脚本
第 15 章 使用 Canvas 绘图
第 16 章 HTML5 脚本编程
第 17 章 错误处理与调试
第 18 章 JavaScript 与 XML
第 19 章 E4X
第 20 章 JSON
第 21 章 Ajax 与 Comet
第 22 章 高级技巧
第 23 章 离线应用与客户端存储
第 24 章 最佳实践
第 25 章 新兴的 API