《JavaScript 权威指南》(第 6 版)中文版笔记
记录《JavaScript 权威指南》(第 6 版)中文版笔记。
1. 概述
JavaScript 是一门高端的、动态的、弱类型的编程语言,非常适合面向对象和函数式编程风格。
JavaScript 早已超出其『脚本语言』(scripting-language)本身的范畴,成为一种集健壮性、高效性和通用性为一身的编程语言。
ECMAScript 是 JavaScript 的语言标准。
ES3 、ES5 就表示 ECMAScript 标准第 3 版、第 5 版,也用 Mozilla 的版本号 JavaScript 1.5 、1.8 表示。
我们常用的 JavaScript 解释器或者『引擎』是 Google 的 V8。
1.1 语言核心
变量是表示值得一个符号名字,变量是通过 var
关键字声明的。
值可以通过等号赋值给变量。
JavaScript 支持多种数据类型:数字(整数、实数共用一种数据类型)、字符串、布尔值、null、undefined 等。
JavaScript 非常重要的两个数据类型是对象和数组。
对象是名/值对的集合,或字符串到值映射的集合,通过 .
或 []
来访问对象属性。
对象和数组中都可以包含另一个对象或数组。
表达式、语句、控制结构(control structure)。
函数:带有名称(named) 和参数的 JavaScript 代码段,可以一次定义多次调用。
将函数和对象合写在一起时,函数就编程了『方法』(method)。
所有的 JavaScript 对象都含有方法。
1.2 客户端 JavaScript
JavaScript 代码可以通过 <script>
标签嵌入到 HTML 文件中。
可以通过 JavaScript 来操控 Web 浏览器中 HTML 内容和文档的 CSS 样式,同样,也可以通过事件处理程序(event handler)来定义文档的行为。
通常我们关心的时间类型是鼠标点击事件和键盘按键事件,或手机中的各种触碰事件。
2. 词法结构
词法结构是一套基础性规则,用来描述如何使用这门语言来编写程序。
2.1 字符集
JavaScript 程序是用 Unicode 字符集编写的。
JavaScript 区分大小写,所有关键字、变量、函数名和标识符必须采用一致的大小写形式。注意: HTML 不区分大小写。
JavaScript 会忽略程序中标识(token)之间的空格,多数情况下,JavaScript 同样会忽略换行符。
Unicode 转义序列由 6 个 ASCII 字符表示任意 16 位 Unicode 内码,以 \u
为前缀,其后跟随 4 个十六进制数。
2.2 注释
JavaScript 支持两种格式的注释:
- 行内注释
//
- 多行注释
/**/
2.3 直接量
直接量( literal),就是程序中直接使用的数据值。
包括数字、小数、字符串、布尔值、正则表达式、null 等。
2.4 标识符和保留字
标识符是用来对变量和函数进行命名,或者用作 JavaScript 代码中某些循环语句中跳转位置标记的名字。
JavaScript 标识符必须以字母、下划线或美元符开始。
保留字是 JavaScript 从标识符中拿出来做关键字的部分,在程序中不能再用这些关键字做标识符了。
这些关键字有
// 已有的
break delete function return typeof
case do if switch var
catch else in this void
continue false instanceof throw while
debugger finally new true with
default for null try
// 未来版本会用到的
class const enum export extends import supper
// 严格模式下是保留字的
implements let private public yield
interface package protected static
// 严格模式下面的标识符不能用作变量名、函数名或参数名
arguments eval
尽量避免使用如下关键字作为标识符,这些关键字在 ES5 中放宽了限制,但在 ES3 中不能用作标识符:
abstract double goto native static
boolean enum implements package super
byte export import private synchronized
char extends int protected throws
class final interface public transient
const float long short volatile
尽量避免用 JavaScript 定义的全局变量和函数名做变量名和函数名:
arguments encodeURI Infinity Number RegExp
Array excodeURIComponent isFinite Object String
Boolean Error isNaN parseFloat SyntaxError
Date eval JSON parseInt TypeError
decodeURI EvalError Math RangeError undefined
decodeURIComponent Function NaN ReferenceError URIError
2.5 可选的分号
JavaScript 使用分号将语句分隔开。
如果每条语句各自独占一行,通常可以省略语句之间的分号。
两种编码风格:一种是任何情况下都使用分号,另一种是能省略分号就省略分号。
在 return
、break
、 continue
和随后的表达式之间不能有换行。
3. 类型、值和变量
类型(type)、值(value)、变量(variable)。
JavaScript 数据类型分为两类:原始类型(primitive type)和对象类型(object type)。
原始类型包括数字、字符串和布尔值。
JavaScript 有两个特殊的原始值:null(空)和 undefined(未定义)。
除上述之外就是对象。对象(object)是属性(property)的集合,每个属性都有『名/值对』构成。有一个特殊对象——全局对象(global object)。数组 (Array) 也是一种特殊对象,另一种特殊对象是函数。
如果函数用来初始化(使用 new 运算符)一个新建的对象,则称之为构造函数(constructor),每个构造函数定义了一类(class)对象——由构造函数初始化的对象组成的集合。
类可以看做是对象类型的子类型,除数组(Array)和函数(Function)类外,JavaScript 语言核心还定义了其他三种有用的类:日期(Date)、正则(RegExp)、错误(Error)。
JavaScript 解释器有自己的内存管理机制,程序员不用担心对象的销毁和内存回收。
JavaScript 是一种面向对象的语言,我们不用全局的定义函数去操作不同类型的值,数据类型本省可以定义方法(method)来使用值。在 JavaScript 中,只有 null 和 undefined 是无法拥有方法的值。
JavaScript 的类型可以分为原始类型和对象类型,也可分为可以拥有方法的类型和不能拥有方法的类型,同样可分为可变(mutable)类型和不可变(immutable)类型。可变类型的值是可修改的。
对象和数组属于可变类型。 数字、布尔值、null 和 undefined 属于不可变类型。
JavaScript 可以自由地进行数据类型转换。
JavaScript 变量是无类型的(untyped),变量可以被赋予任何类型的值,同样一个变量也可以重新赋予不同类型的值。使用 var
关键字来声明(declare)变量,ES6 中推荐使用 const,,如果不能使用 const 就使用 let,不要使用 var。
JavaScript 采用词法作用域(lexical scoping)。
不在任何函数内声明的变量称为全局变量(global variable),在 JavaScript 程序的任何地方都是可见的。在函数内声明的变量具有函数作用域(function scope),只在函数内可见。
3.1 数字
JavaScript 中所有数字均用浮点数值表示。采用 IEEE 754 标准定义的 64 位浮点格式表示数字,能够表示的整数范围为 $(-2^{53} ~ 2^{53})$ 。在实际操作中(数组索引、位操作符等)是基于 32 位整数。
直接出现在 JavaScript 程序中的数字,称为数字直接量(numeric literal)。
注意,在任何数字直接量前添加负号可以得到它们的负值,但负号是一元求反运算符,并不是数字直接量语法的组成部分。
数字直接量包括:
- 整型直接量(包括以 0x 开头的十六进制值)
- 浮点型直接量(还可以用指数计数法表示浮点型直接量)
基本算数运算符:+(加) 、-(减) 、*(乘) 、/(除) 、%(求余)
复杂运算通过 Math 对象的属性定义的函数和常量来实现。
// 常用的 Math 属性函数和常量
Math.pow(2, 53); //=>9007199254740992:2的53次幂
Math.round(0.6); //=>1.0:四舍五入
Math.ceil(0.6); //=>1.0:向上求整
Math.floor(0.6); //=>0.0:向下求整
Math.abs(-5); //=>5:求绝对值
Math.max(x, y, z); //返回最大值
Math.min(x, y, z); //返回最小值
Math.random(); //生成一个大于等于0小于1.0的伪随机数
Math.PI; //π:圆周率
Math.E; //e:自然对数的底数
Math.sqrt(3); //3的平方根
Math.pow(3, 1 / 3); //3的立方根
Math.sin(0); //三角函数:还有Math.cos,Math.atan等
Math.log(10); //10的自然对数
Math.log(100) / Math.LN10; //以10为底100的对数
Math.log(512) / Math.LN2; //以2为底512的对数
Math.exp(3); //e的三次幂
JavaScript 算数运算在溢出(overflow)、下溢(underflow)或被零整除时不会报错。
溢出时,结果为一个特殊的无穷大(infinity)值,正数溢出用 Infinity 表示,负数溢出用 -Infinity 表示。
下溢是当运算结果无限接近于零并比 JavaScript 能表示的最小值还小的时候发生的一种情况。JavaScript 会返回 0 。当一个负数发生下溢是,返回一个特殊值『负零』,负零和零几乎完全一样,但很少用到负零。
被零整除在 JavaScript 中并不会报错,只是返回一个无穷大(Infinity)或负无穷大(-Infinity)。零除以零是没有意义的,结果是一个非数字(not-a-number),用 NaN 表示。
无穷大除以无穷大,给任意负数开方,或算数运算符与不是数字或无法转换为数字的操作数一起使用时都将返回 NaN 。
Infinity 和 NaN 是 JavaScript 预定义的全局变量,在 ES3 中是可读写的,在 ES5 中为只读。
ES3 中,Number 对象定义的属性值也是只读的。
JavaScript 中的非数字值(NaN)不与任何值相等,包括自身。即 NaN == NaN
为 false 。如果要判断一个变量是否为 NaN ,不能通过 x == NaN
来判断,相反,应该使用 x != NaN
来判断,当且仅当 x 为 NaN 时,表达式结果才为 true 。函数 isNaN()
作用类似,另一个函数 isFinite()
在参数不是 NaN 、Infinity 或 -Infinity 时返回 true。
负零与正零相等,但正无穷大与负无穷大不等。
JavaScript 通过浮点数形式只能表示有限个实数(18 437 736 874 454 810 627 个),JavaScript 表示实数时,常常只是真实值得一个近似表示。
现代编程语言所采用的浮点数表示法,是一种二进制表示法,可以精确表示 1/2 、1/8 、 1/1024 等分数,但并不能精确表示 1/10 、1/100 等分数,即不能精确表示类似 0.1 这样简单的数字。可能会出现的情况比如 0.3 - 0.2 == 0.2 - 0.1
返回值为 false,二者并不相等,说明两者的近似值不相等。在任何使用二进制浮点数编程语言中都会出现这个问题。
JavaScript 语言核心用 Date() 构造函数创建表示日期和时间的对象。
3.2 文本
字符串(string)是由一组 16 位值组成的不可变的有序序列。JavaScript 中没有表示单个字符的『字符型』,表示字符只需要将其值付给字符串变量即可。JavaScript 采用 UTF-16 编码的 Unicode 字符集表示字符串,常用 Unicode 字符通过 16 位内码表示,并表示字符串中的单个字符,不能用 16 位 Unicode 字符表示的用两个 16 位值组成的一个序列(代理项对)表示。
字符串直接量是由单引号或双引号括起来的字符序列。由单引号界定的字符串中可以包括双引号,同样由双引号界定的字符串中也可以包含单引号。
如果要把字符串直接量拆分成多行,每行必须以反斜线(\)结束。
JavaScript 和 HTML 都可以单双引号表示字符串,需要约定两种不同书写格式。一般地,JavaScript 用单引号表示字符串,HTML 用双引号。
转义字符(escape sequence)用反斜线 () 后加一个字符表示。
JavaScript 转义字符及含义
转义字符 | 含义 |
---|---|
\o | NUL 字符(\u0000) |
\b | 退格符(\u0008) |
\t | 水平制表符(\u0009) |
\n | 换行符(\u000A) |
\v | 垂直制表符(\u000B) |
\f | 换页符(\u000C) |
\r | 回车符(\u0000D) |
“ | 双引号(\u0022) |
‘ | 撇号或单引号(\u0027) |
\ | 反斜线(\u005C) |
\xXX | 由两位十六进制数 XX 指定的 Latin-1 字符 |
\xXXXX | 由四位十六进制数 XXXX 指定的 Unicode 字符 |
加号运算符用于字符串,表示字符串连接。
在 JavaScript 中字符串是固定不变的。字符串可以当做只读数组。
正则表达式:
两条斜线之间的文本构成了一个正则表达式直接量。
3.3 布尔值
布尔值指代真或假、开或关、是或否。只有两个值,保留字 true 和 false。
undefined
, null
, 0
, -0
, NaN
, ""
会被转换成 false,所有其他值,包括数组和对象都会转换成 true。
3.4 null 和 undefined
null 常用来描述『空值』,null 是一个特殊的对象值,含义是『非对象』。
undefined 的值就是『未定义』,表明变量没有初始化,如果查找数组元素或对象属性返回 undefined 则说明这个元素或属性不存在,如果函数没有任何返回值,返回 undefined。
null 和 undefined 都不柏阔任何属性和方法,用 『.』 和 『[]』来存取这两个值得成员或方法都会产生一个类型错误。
可以认为 undefined 表示系统级的、出乎意料的或类似错误的值得空缺。
null 表示程序级的、正常的或在意料中的值得空缺。
如果要将它们赋值给变量或者属性,或将它们作为参数传入函数,最佳选择是使用 null。
3.5 全局对象
全局对象的属性是全局定义的符号,JavaScript 程序可以直接使用,当 JavaScript 解释器启动时,它将创建一个新的全局对象,并给它一组定义的初始属性:
- 全局属性,undefined, Infinity,NaN
- 全局函数,isNaN(), parseInt(), eval()
- 构造函数,Date(), RegExp(), String(), Object(), Array()
- 全局对象,Math, JSON
全局对象初始属性不是保留字,但应当当做保留字对待。
客户端 JavaScript 的全局对象是 Window, 它自身有一个 window 属性指代 this。
3.6 包装对象
字符串不是对象,但有属性,是因为在引用字符串的属性时,JavaScript 将字符串通过调用 new String()
的方式转换成对象,这个对象继承了字符串的方法,并被用来处理属性的引用。
数字和布尔值也具有各自的方法:通过 Number() 和 Boolean() 构造函数创建临时对象。
null 和 undefined 没有包装对象:访问它们的属性会造成一个类型错误。
在读取字符串、数字和布尔值的属性值(或方法)的时候,表现的像对象一样。但如果你试图给其属性赋值,则会忽略这个操作:修改只是发生在临时对象身上,而这个临时对象并未继续保留下来。
存取字符串、数字或布尔值的属性时创建的临时对象称做包装对象,它只是偶尔用来区分字符串值和字符串对象、数字和数值对象以及布尔值和布尔对象。
可以通过 String(), Number() 或 Boolean() 构造函数显式创建包装对象。
-
==
等于运算符将原始值和其包装对象视为相等。 -
===
全等运算符将它们视为不等。
3.7 不可变的原始值和可变的对象引用
- 原始值:undefined、null、布尔值、数字、字符串
- 对象:数组、对象、函数
两者有根本区别,原始值不可更改,对象是可变的。
原始值的比较是值的比较,对象的比较并非值的比较:即使两个对象包含同样的属性及相同的值,它们也不相等。
通常称对象为引用类型(reference type)。
对象值都是引用,对象的比较均是引用的比较:当且仅当它们引用同一个基对象时,它们才相等。
var a = []; //定义一个引用空数组的变量a
var b = a; //变量b引用同一个数组
b[0] = 1; //通过变量b来修改引用的数组
a[0]; //=>1:变量a也会修改
a === b; //=>true:a和b引用同一个数组,因此它们相等
以上代码只是赋值的引用,对象本身并没有被复制一次。如果要得到一个对象或数组的副本,必须显示复制对象的每个属性或数组的每个元素。可以通过循环语句来完成复制。
如果要比较两个对象或数组,应比较它们的属性或元素。
3.8 类型转换
JavaScript 的 ==
运算符能按照期望的数据类型对两边的数据进行类型转换。
显式转换可以使用 Boolean(), Number(), String() 或 Object() 函数。例如
Boolean({});
Number('5');
String(false);
Object(3);
除了 null 和 undefined 之外任何值都具有 toString() 方法,执行结果返回值与 String() 函数返回值一致。
将 null 或 undefined 转换成对象,会抛出 TypeError 异常,而使用 Object() 函数则会返回一个创建的空对象。
+
运算符一个操作数是字符串,JavaScript 会隐式将另一个操作数转换为字符串
一元 『+』运算符将其操作数转换为数字
一元『!』 运算符将其操作数转换为布尔值并取反,用两个『!』则可以达到 Boolean() 的效果
JavaScript 的 toString() 方法提供精确的数字到字符串转换,toString() 可接受表示转换基数(radix)的可选参数。默认是十进制,可以转换成其他进制,范围是 2~3 之间。
数字到字符串的转换,提供小数点位置、有效数字位数、指数计数法三个方法
- toFixed() 根据小数点后指定位数转换数字到字符串,不是用指数计数法
- toExponential() 使用指数计数法将数字转换为指数形式的字符串
- toPrecision() 根据指定的有效数字位数转换,如果有效数字位数少于整数部分,则用指数形式
三种方法都会四舍五入或填充 0.
全局函数 parseInt() 和 parseFloat() 会解析整数或整数和浮点数。并且两者都会忽略任意数量前导空格,并忽略数值后面的内容,如果第一个非空字符是非法的数字直接量,将反转 NaN。
对象到原始值转换:
- 所有对象转换成布尔值都是 true,包括数字和函数
- 对象到字符转换
- toString() 方法,默认对象的 toString() 转换结果是 『[object Object]』,数组 toString() 方法返回每个数组元素组成的字符串,所有对象都继承了这个方法,还有函数、日期、RegExp、
- 所有对象继承的另一个方法是 valueOf()。如果存在原始值,则将对象转换为它的原始值,而大多数对象无法真正表示为一个原始值,因此默认简单返回对象本身。
对象到字符串的转换:
- 如果对象有 toString() 方法,调用这个方法,并进行转换
- 如果对象没有 toString() 方法,或这个方法并不返回一个原始值,那么会调用 valueOf() 方法
- 如果两者都不能获得原始值,抛出异常。
对象到数字的转换:
- 先尝试使用 valueOf() 方法,将原始值转换为数字
- 否则,使用 toString() 方法
- 否则,抛出异常
『+』、『==』、『!=』和关系运算符是唯一执行字符串到原始值的转换方式的运算符。
『-』运算符把两个操作数都转换为数字。
3.9 变量声明
ES5 中声明变量用 var,使用一个变量之前要声明它,但 JavaScript 里不声明变量直接使用,也不会报错,这会造成很多的麻烦。
JavaScript 中未声明一个变量就使用该变量,解释器会声明一个全局变量。为了减少不必要的麻烦,使用一个变量之前一定要声明它,或者使用 ES6 的 let 或者 const 声明变量。一般情况下尽量使用 const 声明变量,如果不能使用 const 就使用 let,不要使用 var。
3.10 变量作用域
JavaScript 在函数内部可以声明一个和全局变量同名的局部变量,此时在函数内部全局变量会被局部变量覆盖掉。
let scope = 'global';
function checkscope() {
let scope = 'local';
return console.log(scope);
}
checkscope(); // => local
console.log(scope); // => global
JavaScript 没有块级作用域(block scope),取而代之地使用函数作用域(function scope):
变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
函数作用域是表示在函数体内声明的所有变量在整个函数体内都是可见的,不论是否有函数嵌套函数。这也被称为声明提前(hoisting),即函数里声明的所有变量都被提前至函数体的顶部。
let scope = 'global';
function functionscope() {
console.log(scope); // => undefined,因为的函数内声明的局部变量 scope 被提前了,并且覆盖掉了全局变量,但不会将其赋值提前。
let scope = 'local';
console.log(scope); // => local,输出局部变量值
}
在使用 JavaScript 编程时,为了使代码能更清晰的反应其作用域,会将函数内的所有变量声明在函数体顶部。
JavaScript 全局变量是全局对象的属性。
JavaScript 是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。
如果把一个局部变量看做是自定义实现的对象的属性,每一段 JavaScript 代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。一个作用域链是一个对象列表或链表。当 JavaScript 需要查找变量 x 的值时(即『变量解析』(variable resolution)),它会从链表第一个对象开始查找,直到找到对象中的 x 属性取得它的值,如果在整个作用域链上没有任何对象含有属性 x ,则会认为这段代码的作用域链上不存在 x ,最终抛出一个引用错误(ReferenceError)异常。
JavaScript 代码中,首先有一个全局对象的作用域链,其次不含嵌套的函数向作用域链添加了定义函数参数和局部变量的对象,嵌套的函数内作用域链至少含有三个对象:全局对象、外层函数定义函数参数和局部变量的对象、嵌套函数定义函数参数和局表变量的对象
TODO: 深入理解作用域链。
作用域链对理解 with 语句和理解闭包概念至关重要。
4. 表达式和运算符
最简单的表达式是『原始表达式』(primary expression),是表达式的最小单位,不包含其他表达式。包括:常量、直接量、关键字和变量。
常见的复杂表达式是由变量或常量以及运算符构成的。
对象和数组初始化表达式被称为『对象直接量』和『数组直接量』。
用一对方括号『[]』初始化数组,方括号内的元素可以用逗号省略,省略的位置会填充 undefined。
对象初始化表达式用花括号『{}』表示。
函数定义表达式,通常包含关键字 function,圆括号,花括号。
属性访问表达式有两种表示方法,一种是用点『.』,另一种是用方括号『[]』。点只能用来访问对象属性,且属性名不能包含空格或其他标点符号;方括号可以访问数组元素和对象属性,访问数组元素时,方括号内是数组元素下标,访问对象属性时方括号内的属性名要用引号包围起来,如果属性名是通过运算得到的,也必须使用方括号。
调用表达式(invocation expression)是一种调用或执行函数或方法的语法表示。函数如果无返回值,那么返回 undefined。
对象创建表达式(object creation expression)创建一个对象并调用一个构造函数初始化新对象的属性,一般包含一个 new 关键字:
new Object();
new Date(); // 不需要传入参数时,圆括号可以省略
JavaScript 中的运算符用于算数表达式、比较表达式、逻辑表达式、赋值表达式等。
运算符包括标点符号(+,=)和关键字(delete,instanceof)。
运算符可按照其操作数个数进行分类,分为一元运算符(unary operator),二元运算符(binary operator),以及三元运算符。JavaScript 支持一个三元运算符(ternary operator)—— ?:。
操作数类型和运算结果类型可能并不相同。JavaScript 通常会根据需要将操作数类型进行转换。
左值:表达式只能出现在赋值运算符的左侧。++,—,变量、对象属性、数组元素均是左值。
副作用:++,— 运算符包含隐式的赋值,delete 运算符也有副作用,删除一个属性就像(但不完全一样)给这个属性赋值 undefined。如果在一个函数体内或构造函数中使用了这些运算符并产生了副作用时,函数调用表达式和对象创建表达式是由副作用的。
TODO: 副作用的实际影响
属性访问表达式和调用表达式的优先级比所有运算符都要高。
进行算术运算时,所有无法转换成数字的操作数都会转换为 NaN 值,如果操作数(或转换结果)是 NaN,算术运算结果也是 NaN。
取余(模)运算结果的符号和第一个操作数(被除数)符合保持一致。
『+』 运算符
加法运算符可以对两个数字做加法,也可以连接字符串。
- 如果其中一个操作数是对象,会遵循对象到原始值的转换规则转换为原始类值
- 转换后,如果其中一个操作数是字符串,另一个也会转为字符串
- 否则两个操作数都将转换为数字(或 NaN),然后进行加法操作。
一元算数运算符(+、-、++、—)
一元算数运算符作用于一个单独的操作数,并产生一个新值。
一元加法将操作数转为数字,并返回转换的数字;一元减号将操作数转为数字,并返回转换的数字的反值。
++x
是前增量(pre-increment),对操作数进行增量计算,并返回计算后的值;x++
是后增量(post-increment),对操作数进行增量计算,但返回未做增量计算的(unincremented)值。常见于 for 循环。--
和 ++
用法相同,作用相反。
位运算符
位运算符要求操作数是 32 位整型,它会对操作数转换为数字,并强制表示为 32 为整型,移位运算符要求右操作数在 0 ~ 31 之间。它将其操作数转换为无符号 32 位整数后,它们将舍弃第 5 位之后的二进制位,一遍生成一个位数正确的数字。 位运算符会将 NaN、Infinity 和 -Infinity 都转换为 0.
- 按为与(&)
对操作数逐位执行布尔与操作,只有对应位都为 1 时结果才为 1 。
- 按位或(|)
对操作数逐位执行布尔或操作,只要对应为有一个为 1 结果就为 1.
- 按位异或(^)
对操作数逐位执行布尔异或操作,只有一个操作数为 1 时结果才为 1 。
- 按位非(~)
一元运算符,位于一个整型参数之前,对操作数所有位取反。JavaScript 中,取反相当于改变操作数符号并减 1。
- 左移(<<)
对一个值左移 1 位相当于乘 2,左移两位相当于乘以 4.
- 带符号右移(>>)
右移高位填补数由原数符号决定,原数为正高位填 0 ,原数为负高位填 1。对一个值右移 1 位相当于除以 2 ,右移两位相当于除以 4 。
- 无符号右移(>>>)
与 『>>』一样,只不过左边高位总是填补 0 。
关系表达式。
==
和 ===
运算符比较两个值是否相等。===
是严格相等(Strict equality)或恒等运算符(identity operator),不允许进行类型转换。==
是相等运算符(equality operator),允许进行类型转换。反过来,!=
是不相等,!==
是严格不相等。
通过 x!==x 来判断 x 是否为 NaN,只有在 x 为 NaN 的时候,这个表达式的值才为 true。
比较运算符包括大于、小于、大于等于、小于等于。比较运算符的操作数可能是任意类型,但只有数字和字符串才能真正执行比较操作,因此不是数字和字符串的操作数都将进行类型转换。类型转换遵循如下规则:
- 如果操作数为对象,那么将这个对象转换为原始值:如果 valueOf() 返回一个原始值,那么直接使用这个原始值。否则,使用 toString() 的转换结果进行比较。
- 在对象转换为原始值后,如果两个操作数都是字符串,那么依照字母表顺序(组成字符串的 16 位 Unicode 字符索引顺序 )进行比较
- 在对象转换为原始值后,如果至少有一个操作数不是字符串,那么两个操作数都将转换为数字进行数值比较。
- 0 和 -0 相等。
- Infinity 比除了它本身任何数字都大
- -Infinity 比除了它本身任何数字都小
- 如果其中一个操作数是(或转换后是)NaN,那么比较结果总是返回 false
进行字符串比较时,要区分字母大小写,大写字母总是小于小写字母。使用 String.localCompare() 方法能更好的比较字符串。对于不区分字母大小写的比较来说,首先要把字符串全部转换为大写或者小写字母,通过 String.toUpperCase() 和 String.toLowerCase()。
加号运算符更倾向于操作字符串,如果其中一个操作数是字符串,则进行字符串连接操作,例如 1 + '2'
结果为 12
。
比较运算符更倾向于操作数字,只有两个操作数都是字符串的时候,才会进行字符串比较,例如 1 < '2'
结果为 true
。
当一个操作数是 NaN 时,所有 4 个比较运算符均返回 false。
in 运算符。
in 运算符希望它的左操作数是一个字符串或可以转换为字符串,希望他的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数的属性名,那么表达式返回 true。
instanceof 运算符。
instanceof 运算符希望左操作数是一个对象,右操作数为对象的类。如果左侧的对象是右侧类的实例,表达式返回 true,否则返回 false。JavaScript 中对象的类是通过初始化它们的构造函数来定义的,因此 instanceof 的右操作数应当是一个函数。
所有的对象都是 Object 的实例。如果 instanceof 的左操作数不是对象,结果为 false;如果右操作数不是函数,则抛出类型错误异常。
要了解 instanceof 如何工作必须先理解**『原型链』(prototype chain)**。
TODO:原型链
逻辑表达式
逻辑与(&&)、逻辑或(||)、逻辑非(!)
关系运算符优先级比 && 和 || 高,因此书写关系表达式之间的逻辑运算不用补圆括号,例如 x > 2 && y < 3
可直接书写。
&& 的操作数不一定是布尔值,有些值例如『false、null、undefined、0、-0、NaN、和 ""』都是假值,所有其他值都是真值。&& 可以对真值和假值进行布尔与操作。
&& 运算符只有两个操作数都为真值是返回一个真值,否则返回一个假值。但这个返回值首先依赖于左操作数,如果做操作数为假,那么整个表达式为假,返回值就是做操作数的值;如果做操作数为真,则表达式返回结果依赖于右操作数,如果右操作数为真值,则表达式为真,返回右操作数值,如果右操作数为假,则表达式为假,返回有操作数值。因此,&& 运算可能不会去计算右操作数。
&& 运算符的行为有时称为『短路』(short circuiting),利用这一特性可以执行如下代码:
if (a == b) stop();
a == b && stop(); //作用同上,只有 a == b 才调用 stop()
|| 运算符理解与 && 类似,只不过当左值为真是表达式就会返回这个真值。|| 运算也可能不会计算右操作数。
|| 运算符最常用的方式是从一组备选表达式中选出一个真值表达式:
// 如果 maxWidth 已定义,直接使用,否则在 preferences 对象中查找 maxWidth
// 如果对象也未定义 maxWidth,则直接使用一个常量
let max = maxWidth || preferences.maxWidth || 500;
这种用法一般用在函数体内,来给参数提供默认值。
! 运算符会对操作数首先转换为布尔值,因此总是返回 true 或 false。
全局函数 eval()
eval() 是函数但已经被当做运算符来对待。
eval() 只有一个参数,如果这个参数不是字符串,则会直接返回这个参数,如果是字符串,则把字符串当成 JavaScript 代码进行编译,如果编译失败则抛出语法错误(SyntaxError)异常,如果编译成功,则执行这段代码。
eval() 具有改变局部变量的能力。ES3 规定任何解释器都不允许对 eval() 赋予别名,否则会抛出 EvalError 异常。
实际上,大多数的实现并不是这么做的。当通过别名调用时,eval()会将其字符串当成顶层的全局代码来执行。执行的代码可能会定义新的全局变量和全局函数,或者给全局变量赋值,但却不能使用或修改主调函数中的局部变量,因此,这不会影响到函数内的代码优化。
ECMAScript 5 是反对使用 EvalError 的,并且规范了 eval()的行为。“直接的 eval”,当直接使用非限定的”eval”名称(eval 看起来像是一个保留字)来调用 eval()函数时,通常称为“直接 eval”(direct eval)。直接调用 eval()时,它总是在调用它的上下文作用域内执行。其他的间接调用则使用全局对象作为其上下文作用域,并且无法读、写、定义局部变量和函数。
在严格模式下使用 eval() 时,eval() 是私有上下文环境中的局部 eval。eval 执行的代码段可以查询或改变局部变量,但不能在局部作用域中定义新的变量或函数。
typeof 运算符
typeof 运算符放在单个操作数前面,返回操作数类型的字符串。
x | typeof x |
---|---|
undefined | ”undefined” |
null | ”object” |
true/false | ”boolean” |
任意 number/NaN | ”number” |
任意 string | ”string” |
任意 function | ”function” |
任意内置 object | ”object” |
任意宿主 object | 由编译器各自实现的字符串,不是”undefined”,“boolean”,“number”或”string” |
当操作数是 null 的时候,typeof 将返回”object”。如果想将 null 和对象区分开,则必须针对特殊值显式检测。
对于宿主对象来说,typeof 有可能并不返回”object”,而返回字符串。但实际上客户端 JavaScript 中的大多数宿主对象都是”object”类型。
由于所有对象和数组的 typeof 运算结果是”object”而不是”function”,因此它对于区分对象和其他原始值来说是很有帮助的。
如果想区分对象的类,则需要使用其他的手段,比如使用 instanceof 运算符(参照 4.9.4 节)、class 特性(参照 6.8.2 节)以及 constructor 属性(参照 6.8.1 节和§9.2.2 节)。
所有可执行对象 的 typeof 返回值都是 “function”,包括内置对象(native object)和宿主对象(host object)。
大多数浏览器将 JavaScript 原生函数对象当成它们的宿主对象的方法来使用,但 IE 除外。
delete 运算符
delete 用来删除对象属性或数组元素。delete 希望其操作数是一个左值,如果不是左值则不进行任何操作返回 true,如果是左值,delete 将试图删除这个指定左值,删除成功返回 true。
并非所有属性都可以删除,一些内置核心和客户端属性不能删除,用户通过 var 语句声明的变量不能删除,通过 function 语句定义的函数和函数参数也不能删除。**在严格模式下,这些删除操作都会报错。
void 运算符
void 出现在操作数之前,操作数可以是任意类型。操作数照常计算,反返回的结果是 undefined。
常用在客户端的 URL——javascript:URL 中,**在 URL 中可以写带有副作用的表达式,而 void 则让浏览器不必显示这个表达式的计算结果。**例如:
<a href="javascript:void window.open();"> Open New Window </a>
逗号运算符(,)
先计算左操作数,再计算右操作数,最后返回右操作数的值。总成会计算左侧表达式,但是忽略掉结果。常用在 for 循环中,例如:
for (let i = 0, j = 10; i < j; i++, j++) {
console.log(i + j);
}
5. 语句
语句(statement)是 JavaScript 整句或命令。
把表达式当做语句的用法称作表达式语句(expression statem)。
声明新变量或定义新函数的语句称作声明语句(declaration statem)。
5.1 表达式语句
- 赋值
- 递加(++)和递减(—)
- delete 语句
- 函数调用
5.2 复合语句和空语句
JavaScript 中可以用花括号将多条语句括起来形成一条复合语句(compound statement)或语句块。
- 语句块结尾不需要分号,但块中的原始语句必须以分号结束;
- 语句块中的行都有缩进,但不是必须的,为了代码可读性更强,更容易理解,需要用缩进;
- JavaScript 没有块级作用域,在语句块中声明的变量并不是语句块私有的。
语句块用在很多地方,if,for,while,等。
空语句表示一行代码除了分号其他什么也没有,自然不会执行任何动作,但它是很有用的,例子:
// 初始化数组 a
for (i = 0; i < a.length; a[i++] = 0);
例子中在 for 循环体内已经完成数组初始化,因此不需要在 for 循环体中包含其他任何语句,但 JavaScript 需要循环体至少包含一条语句,故而空语句起到了了作用。
需要注意的是,在 JavaScript 的 for 循环、while 循环或 if 语句右圆括号后的使用分号很容易被忽略,如果错误使用分号则会导致致命 bug,而且这些 bug 难以定位,因此如果要使用空语句最好进行注释说明。
5.3 声明语句
- var
用 var 声明一个或者多个变量,不过最好不要用 var,而应该用 ES6 的 let 或 const。
- function
函数声明语句通常出现在代码顶层,一个函数由 function,funcname,arguments,{},以及花括号内的语句块构成。
函数声明语句与函数定义表达式二者不同之处
- 函数声明语句中的函数名是一个变量名,变量指向函数对象
- 函数定义语句中的整个函数被显式地“提前”到了脚本或函数顶部;使用 var 的话,只有变量声明提前了,变量初始化代码仍然在原来的位置
5.4 条件语句
条件语句包括 if/else
和 switch
。
关于 switch 语句需要注意的是:case 在进行比较时,是按照===
运算符进行比较的。
switch 语句的每一个 case 也可以用 return 代替 break,这种情况主要用在函数体内。
每个 case 关键字可以跟随任意表达式,但最安全的做法是在 case 表达式中使用常量表达式。
switch 语句中的 default 语句块一般出现在末尾,但其实可以出现在任何位置。
5.5 循环
JavaScript 有 4 中循环语句:while,do/while,for,for/in.
while 与 do/while 区别在于:
- while 首先会检测循环表达式,表达式为真,执行循环体,表达式为假,不执行循环体;而 do/while 会先执行循环体,后检测循环表达式,因此循环体至少会被执行一次。
- do/while 循环必须使用关键字 do 来标识循环开始,用 while 来标识循环结尾并进入循环条件判断。
- do/while 循环以分号结尾
for 循环:
for 循环的三个表达式中的任何一个都可以省略,但两个分号是必不可少的。
for/in 循环:
for (variable in object) {
statement;
}
for/in 循环只会遍历可枚举(enumerable)的属性。
一般情况下,属性枚举顺序按照属性定义的先后顺序来枚举简单对象属性。在以下情况中,枚举的顺序取决于具体实现(并且是非交互的):
- 对象继承了可枚举属性
- 对象具有整数数组索引的属性
- 使用 delete 删除了对象已有的属性
- 使用 Object.defineProperty() 或者类似的方法改变了对象的属性
除了所有非继承的“自有”属性以外的继承属性往往都是可枚举的,而且按照他们定义的顺序进行枚举。
数组索引是非数字或数组时稀疏数组时他们按照特定顺序枚举。
5.6 跳转
- 标签语句
通过在语句天添加标识符和冒号为语句添加标签,就可以在程序的任何地方拖过标签名引用这条语句,但首先要给语句块定义标签才会有用。break 和 continue 是 JavaScript 中唯一可以使用语句标签的语句。
mainloop: while (token != null) {
...
continue mainloop;
...
}
标签的命名空间和变量或函数的命名空间是不同的,因此可以使用同一个标识符作为语句标签和作为变量名或函数名。语句标签只有在它所起作用的语句(当然也可以在它的子句中)内是有定义的。一个语句标签不能和它内部的语句标签重名,但在两个代码段不相互嵌套的情况下是可以出现同名的语句标签的。带有标签的语句还可以带有标签,也就是说,任何语句可以有很多个标签。
- break 语句
单独使用 break 语句立即退出最内层循环或 switch 语句。
break 后面也可以跟随一个以定义的语句标签:
break labelname;
程序会跳转到这个标签所标识的语句块的结束,或直接终止这个闭合语句块的执行。
break 语句控制权无法越过函数边界,无论有没有标签。
- continue 语句
continue 语句是结束本次循环转而执行下一次循环。语法与 break 一样。
- return 语句
return 语句只能在函数体内出现,否则会报语法错误。
- throw 语句
抛出程序异常。
- try/catch/finally 语句
首先执行 try 中代码,当发生错误是通过 catch 抛出异常,最终会执行 finally 中的代码。语句中的 finally 会一直在最后被执行。解释器会忽略掉 finally 中的 return,continue,break 或 throw。
5.7 其他语句类型
with,debugger,“use strict”.
with 用于临时扩展作用域链,语法为:
with (object) statement;
普通模式下尽量避免使用 with,在严格模式下禁止使用 with。
在对象嵌套层次很深的时候通常会使用 with 语句简化代码编写。
debugger 用手在调试程序时在代码中添加断点。
“use strict” 是 ES5 引入的一条指令,而非语句,它与普通语句的之间的区别是:
- 它不包含任何语言的关键字,指令仅仅是一个包含一个特殊字符串直接量的表达式(可以是使用单引号也可以使用双引号),对于那些没有实现 ECMAScript 5 的 JavaScript 解释器来说,它只是一条没有副作用的表达式语句,它什么也没做。将来的 ECMAScript 标准希望将 use 用做关键字,这样就可以省略引号了。
- 它只能出现在脚本代码的开始或者函数体的开始、任何实体语句之前。但它不必一定出现在脚本的首行或函数体内的首行,因为”use strict”指令之后或之前都可能有其他字符串直接量表达式语句,并且 JavaScript 的具体实现可能将它们解析为解释器自有的指令。在脚本或者函数体内第一条常规语句之后字符串直接量表达式语句只当做普通的表达式语句对待;它们不会当做指令解析,它们也没有任何副作用。
使用 “use strict” 表示代码是严格代码。
严格模式与非严格模式区别:
- 严格模式中禁止使用 with *
- 严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、catch 从句参数或全局对象的属性赋值,将会抛出一个引用错误异常。而非严格模式会隐式声明一个全局变量。*
- 严格模式中,调用函数中的一个 this 值是 undefined。非严格模式下函数中的 this 值总是全局对象。*
- 严格模式中,通过 call() 或 apply() 来调用函数时,其中的 this 值就是通过 call() 或 apply() 传入的第一个参数。在非严格模式中, null 和 undefined 值被全局对象和转换为对象的非对象值所代替。
- 严格模式中,给只读属性值和给不可扩展的对象创建新成员都将抛出一个类型错误异常。在非严格模式中,这些操作只是简单地操作失败,不会报错。
- 严格模式中,传入 eval() 的代码不能在调用程序所在的上下文中声明变量或定义函数,而在非严格模式中是可以这样做的。相反,变量和函数的定义是在 eval()创建的新作用域中,这个作用域在 eval()返回时就弃用了。
- 在严格模式中,函数里的 arguments 对象(见 8.3.2 节)拥有传入函数值的静态副本。在非严格模式中,arguments 对象具有“魔术般”的行为,arguments 里的数组元素和函数参数都是指向同一个值的引用。
- 在严格模式中,当 delete 运算符后跟随非法的标识符(比如变量、函数、函数参数)时,将会抛出一个语法错误异常(在非严格模式中,这种 delete 表达式什么也没做,并返回 false)。
- 在严格模式中,试图删除一个不可配置的属性将抛出一个类型错误异常(在非严格模式中,delete 表达式操作失败,并返回 false)。
- 在严格模式中,在一个对象直接量中定义两个或多个同名属性将产生一个语法错误(在非严格模式中不会报错)。
- 在严格模式中,函数声明中存在两个或多个同名的参数将产生一个语法错误(在非严格模式中不会报错)。
- 在严格模式中是不允许使用八进制整数直接量(以 0 为前缀,而不是 0x 为前缀)的(在非严格模式中某些实现是允许八进制整数直接量的)。
- 在严格模式中,标识符 eval 和 arguments 当做关键字,它们的值是不能更改的。不能给这些标识符赋值,也不能把它们声明为变量、用做函数名、用做函数参数或用做 catch 块的标识符。
- 在严格模式中限制了对调用栈的检测能力,在严格模式的函数中,arguments.caller 和 arguments.callee 都会抛出一个类型错误异常。严格模式的函数同样具有 caller 和 arguments 属性,当访问这两个属性时将抛出类型错误异常(有一些 JavaScript 的实现在非严格模式里定义了这些非标准的属性)。
6. 对象
对象可以看做是属性的无序集合,也可以看成是从字符串到值的映射。
除了字符串、数字、true、false、null 和 undefined 之外,JavaScript 中的值都是对象。
通过引用来操作对象。如果变量 x 是指向一个对象的引用,那么执行代码 var y = x;
变量 y 也是指向同一个对象的引用,而非这个对象的副本。通过变量 y 修改这个对象亦会对变量 x 造成影响。
对象最常见的用法是创建(create)、设置(set)、查找(query)、删除(delete)、检测(test)和枚举(enumerate)它的属性。
属性名是可以包含空字符串在内的任意字符串,但不能存在两个同名属性。
值可以是任意 JavaScript 值,或者一个 getter 或 setter 函数(或两者都有)。
属性特性(property attribute)包括:
- 可写(writable attribute),表明是否可以设置该属性的值
- 可枚举(enumeralbe attribute),表明是否可以通过 for/in 循环返回该属性
- 可配置(configurable attribute),表明是否可以删除或修改该属性。
对象的对象特性(object attribute):
- 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象。
- 对象的类(class)是一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag)指明了(在 ECMAScript 5 中)是否可以向该对象添加新属性。
三类对象和两类属性:
- 内置对象(native object)是由 ECMAScript 规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。
- 宿主对象(host object)是由 JavaScript 解释器所嵌入的宿主环境(比如 Web 浏览器)定义的。客户端 JavaScript 中表示网页结构的 HTMLElement 对象均是宿主对象。既然宿主环境定义的方法可以当成普通的 JavaScript 函数对象,那么宿主对象也可以当成内置对象。
- 自定义对象(user-defined object)是由运行中的 JavaScript 代码创建的对象。
- 自有属性(own property)是直接在对象中定义的属性。
- 继承属性(inherited property)是在对象的原型对象中定义的属性。
6.1 创建对象
创建对象的方式有:对象直接量、关键字 new 和 Object.create()
对象直接量是一个表达式,这个表达式的每次运算都创建并初始化一个新的对象。每次计算对象直接量的时候,也都会计算它的每个属性的值。也就是说,如果在一个重复调用的函数中的循环体内使用了对象直接量,它将创建很多新对象,并且每次创建的对象的属性值也有可能不同。
每一个对象都从原型继承属性。一个对象可能继承自多个原型,这些链接的原型对象称为“原型链”。
Object.create() 包含两个参数,第一个参数是这个对象的原型,第二个参数是可选的,用以对对象属性进行进一步描述。
Object.create()是一个静态函数,而不是提供给某个对象调用的方法。
// 使用方法一、直接传入对象
let o1 = Object.create({ x: 1, y: 2 });
// 使用方法二、通过传入 null 创建没有原型的对象
let o2 = Object.create(null);
// 使用方法三、创建普通空对象,传入 Object.prototype
let o3 = Object.create(Object.prototype);
可以通过任意原型创建新对象(换句话说,可以使任意对象可继承)
通过原型继承创建一个新对象
//inherit()返回了一个继承自原型对象p的属性的新对象
//这里使用ECMAScript 5中的Object.create()函数(如果存在的话)
//如果不存在Object.create(),则退化使用其他方法
function inherit(p){
if(p==null)throw TypeError();//p是一个对象,但不能是null
if(Object.create)//如果Object.create()存在
return Object.create(p);//直接使用它
var t=typeof p;//否则进行进一步检测
if(t!=="object"&&t!=="function")throw TypeError();
function f(){};//定义一个空构造函数
f.prototype=p;//将其原型属性设置为p
return new f();//使用f()创建p的继承对象
}
inherit()函数的其中一个用途就是防止库函数无意间(非恶意地)修改那些不受你控制的对象。不是将对象直接作为参数传入函数,而是将它的继承对象传入函数。当函数读取继承对象的属性时,实际上读取的是继承来的值。
6.2 属性的查询和设置
通过 .
或 []
来获取属性值。
通过 []
获取属性值更像是数组,这种数组称作关联数组(associative array),也称作散列、映射或字典(dictionary)。
JavaScript 对象都是关联数组。
属性赋值操作只会在原始对象上创建属性或对已有属性赋值,而不会去修改原型链。
查询一个对象及其原型都不存在的属性返回 undefined,查询一个不存在的对象的属性会报错。
举例:避免上述报错的方法
// 1.
let len = undefined;
if (book) {
if (book.subtitle) {
len = books.subtitle.length;
}
}
// 2.
let len = book && book.subtitle && books.subtitle.length;
给 null 和 undefined 设置属性也会报类型错误。
给只读属性赋值,给不允许新增属性对象添加属性,在普通模式下不会报错,但在严格模式下任何失败的属性设置操作都会抛出一个类型错误异常。
6.3 删除属性
delete 删除对象的属性,它的操作数是一个属性访问表达式,delete 只是断开属性和宿主对象的联系,而不会去操作属性中的属性。
delete 只能删除自有属性,继承属性必须从定义这个属性的原型对象上删除它,并且会影响所有继承自这个原型的对象。
delete 不能删除不可配置属性,不能删除全局变量,不能删除全局函数
6.4 检测属性
用 in
运算符、hasOwnProperty()
和 propertyIsEnumerable()
判断某个属性是否存在于某个对象中。
用法:
let o = { x: 1 };
// in
'x' in o;
// hasOwnProperty()
o.hasOwnProperty('x');
// propertyIsEnumerable()
// 该方法只有在自有属性可枚举才返回 true
o.propertyIsenumerabe('x');
也可以用 !==
判断一个属性是否为 undefined,对象中存在某个属性,!== undefined
返回 true,否则返回 false。但如果这个属性值本身为 undefined 时,只能使用 in 运算符。
6.5 枚举属性
继承的属性、方法也是可以被枚举的,如果不想枚举这些属性,则需要过滤,主要通过两种方法过滤:
// 1
for (p in o) {
if (!o.hasOwnProperty(p)) continue; // 跳过继承的属性
}
//2
for (p in o) {
if (typeof o[p] === 'function') continue; //跳过方法
}
枚举属性名称函数 Object.keys()
和 Object.getOwnPropertyNames()
,第二个函数返回对象所有自有属性名称,而不仅仅是可枚举属性。
6.6 属性 getter 和 setter
ES5 中属性值可以用 getter 和 setter 方法替代。
由 getter 和 setter 定义的属性称作存取器属性(accessor property),一般的的属性称作数据属性(data property),数据属性只有一个简单的值。
存取器属性不具有可写性(writable attribute)。
- 如果属性同时具有 getter 和 setter 方法,它是一个可读/写属性
- 如果只有 getter 方法,它是一个只读属性
- 如果只有 setter 方法,它是一个只写属性,读取只写属性返回 undefined
定义存取器属性方法:
var o = {
//普通的数据属性
data_prop: value, //存取器属性都是成对定义的函数
get accessor_prop() {
/*这里是函数体*/
},
set accessor_prop(value) {
/*这里是函数体*/
},
};
存取器属性可以继承。
6.7 属性的特性
可写、可枚举、可配置。
- 数据属性包含 4 个特性:值(value)、可写性(writable)、可枚举性(enumerable)、可配置性(configurable)
- 存取器属性包含 4 个特性:读取(get)、写入(set)、可枚举性、可配置性
属性描述符(property descriptor)对象用来实现属性特性的查询和设置操作,这个对象具有上述 4 个特性的属性。
通过调用 Object.getOwnPropertyDescriptor()
获得某个对象特性属性的属性描述符。
设置属性特性或想要新建属性具有某种特性,需要调用 Object.defineProperty()
,传入要修改的对象、要创建或修改的属性名称以及属性描述符对象。
传入的描述符对象不必包含所有四个特性。且此方法只能修改已有属性和新建自有属性,不能修改继承属性
同时修改或创建多个属性,使用 Object.defineProperties()
。第一个参数是要修改的对象,第二个参数是一个映射表,包含要新建或修改的属性名称以及他们的属性描述符。
对于不允许创建或修改的属性,使用 Object.defineProperty()
和 Object.defineProperties()
对其操作会抛出类型错误异常。
6.8 对象的三个属性
每一个对象都有与之相关的原型(prototype)、类(class)和可扩展性(extensible attribute)。
- 原型属性
对象的原型属性是用来继承属性的。
通过将对象作为参数传入 Object.getPrototypeOf()
查询对象原型。
使用 isPrototypeOf()
检测一个对象是否是另一个对象的原型(或处于原型链中)。
- 类属性
要想获得对象的类,可以通过调用对象的 toString() 方法,然后提取返回字符串的第 8 个到倒数第二个位置之间的字符。
classof() 实现:
function classof(0) {
if(o === null) return "Null";
if(o === undeifined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
- 可扩展性
表示是否可以给都对象添加新属性。所有内置对象和自定义对象都是显示可扩展的。宿主对象的可扩展性有 JavaScript 引擎定义。
通过将对象作为参数传入 Object.preventExtensions()
将对象转换为不可扩展对象。一旦转换为不可扩展对象,则不能再将其转换为可扩展对象。该方法只影响对象本身,如果给该对象原型添加属性,则即使该对象为不可扩展,也会继承原型中所添加的新属性。
Object.seal() 除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。已经 sealed 的对象不能解封。使用 Object.isSealed() 检测对象是否封闭。
Object.freeze() 将更严格地锁定对象——冻结(frozen)。除了能实现以上两个方法的功能,还可以将自有的所有数据属性设置为只读。用 Object.isFrozen() 检测对象是否冻结。
6.9 序列化对象
将对象的状态转换为字符串,也可将字符串还原为对象。
通过 JSON.stringfy()和 JSON.parse() 实现。
可以引入 http://json.org/json2.js 在 ES3 环境使用上述函数。
6.10 对象方法
- hasOwnProperty(),
- propertyIsEnumerable(),
- isPrototypeOf(),
- Object.create(),
- Object.getPrototypeOf(),
- toString(),
- toLocaleString()
- toJSON()
- valueOf()
7. 数组
JavaScript 数组时无类型的,数组庸俗课程是任意类型,并且数组中不算元素也可能有不同类型,数组的元素也可能是对象或其他数组,因此可以创建复杂的数据结构,如数组的数组和对象的数组。
JavaScript 数组最大索引是 2^32-2(4 294 967 294).
创建 JavaScript 数组时无需给定大小。稀疏数组的索引不一定连续。
JavaScript 数组是 JavaScript 对象的特殊形式。
7.1 创建数组
- 使用数组直接量创建
let arrayName = [];
数组直接量语法允许有可选的结尾逗号,[,,] 只有两个元素而非三个。
- 调用构造函数 Array()
// 调用时没有参数,创建一个空数组,相当于使用直接量创建数组
let a = new Array();
// 调用时传一个数值参数,指定数组长度
let b = new Array(10);
//显示指定两个或多个元素或数组的一个非数值元素
let c = new Array(1, 2, 3, 4, 5, 'test');
7.2 数组元素的读和写
访问数组元素用 [] 操作符,方括号内是一个返回非符整数值的任意表达式。方括号内即可以是数组索引也可以是数组元素。数组是对象的特殊形式,使用方括号访问数组元素就像使用方括号访问对象属性一样。
数组的索引和对象的属性名相似,区别在于只有在 0 ~ 2^32-2 之间的整数属性名才是索引。数组的特别之处在于对于在 0 ~ 2^32-2 之间的属性名会自动维护数组 length 属性值。
可以使用负数或非整数来索引数组。这种情况下,数值转换为字符串,字符串作为属性名来用。既然名字不是非负整数,它就只能当做常规的对象属性,而非数组的索引。
当试图查询任何对象中不存在的属性时,不会报错,只会得到 undefined 值。
7.3 稀疏数组
用 Array() 构造函数或指定数组索引值大于当前数组长度来创建稀疏数组。
也可以用 delete 操作符产生稀疏数组。
需要注意的是,在数组直接量中省略值时不会创建稀疏数组,省略的元素是 undefined 。(这里的省略值是指使用直接量语法创建数组时数组中的所有值都被省略)???使用 node 验证后发现,此方法创建的数组也无元素。
let a = [,,,,]
``()`
如果省略数组直接量中的某几个值,所得到的数组时稀疏数组。
```javascript
let b = [1,,3]
7.4 数组长度
数组长度必定大于每个元素索引值。
- 如果赋值元素的索引 a 大于等于数组长度,数组 length 属性值将设为 a + 1
- 如果设置 length 属性小于当前长度非负整数 n 时,当前数组索引值大于等于 n 的元素将被删除。
设置数组 length 属性为只读:
Object.defineProperty(arrayName, 'length', (writable: false));
7.5 添加和删除数组元素
添加数组元素
- 为新索引赋值
- 使用 push() 方法在数组末尾增加一个或多个元素 3.
- 使用 unshift() 在数组首部插入一个元素,并把其他元素依次移到更高索引处
删除数组元素
- 使用 delete 运算符删除数组元素,会让数组变成一个稀疏数组
- 使用 pop() 方法,每次减少长度 1 并返回被删除元素的值(与 push()对应 )
- 使用 shift() 从数组头部删除一个元素(与 unshift()对应)。
7.6 遍历数组
使用 for 循环
使用 for/in 循环处理稀疏数组
for (var i in a) {
if (!a.hasOwnProperty(i)) continue; //跳过继承的属性
//循环体
}
for (var i in a) {
//跳过不是非负整数的i
if (String(Math.floor(Math.abs(Number(i)))) !== i) continue;
}
使用 forEach() 方法
按照索引顺序按个传递数组元素给定义的函数。
7.7 多维数组
JavaScript 不支持真正的多维数组,但可以用数组的数组来近似。即数组的每一个元素也是一个数组,访问二维数组元素时用两个索引即可,例如 array[x][y]
7.8 数组方法
- join()
将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。
- reverse()
将数组中的元素颠倒顺序,返回逆序的数组。
- sort()
将数组中的元素排序并返回排序后的数组。
可以给该方法传递参数
- 当不传参数调用该方法时,数组元素以字母表顺序排序
- 如果要以其他方式排序,必须给该方法传递一个比较函数
该函数决定了它的两个参数在排好序的数组中的先后顺序 函数应有两个参数,函数的返回值分为负数、0、正数 当第一个参数应该在前时,返回负数;第一个参数应该在后时,返回正数;当两个参数相等时,返回 0。
-
concat()
-
slice()
-
splice()
-
push() 和 pop()
-
unshift() 和 shift()
-
toString() 和 toLocaleString()