《JavaScript高级程序设计 第三版》读书笔记 - 第四至六章
- 变量、作用域和内存问题
- 引用类型
- 面向对象的程序设计
第4章 变量、作用域和内存问题
基本类型和引用类型的值
基本类型的值,在赋值时会复制一份,两个变量是相互独立的。
引用类型的值,在赋值时相当于修改指针,使他们指向同一个内存中的对象。
函数传参时,基本类型的值会复制一份传进函数内的局部变量;引用类型的值相当于把指针复制一份传进函数内的局部变量。
instanceof
操作符用来查询某个对象的类型:
1 | result = variable instanceof constructor |
如果是该种引用类型,则返回true
执行环境及作用域
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
在Web浏览器中,全局执行环境被认为是window
对象。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。
JavaScript中没有块级作用域。
使用var
声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;在with
语句中,最接近的环境是函数环境。如果初始化变量时没有使用var
声明,该变量会自动被添加到全局环境。
垃圾收集
离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
一旦数据不再有用,最好通过将其值设置为null
来释放其引用——这个做法叫做解除引用。
第5章 引用类型
Object
类型
使用对象字面量表示法创建Object
实例:
1 | var person = { |
属性名也可以使用字符串和纯数字。
可以用上述表示法向函数传参。
使用方括号访问属性:
1 | alert(person["name"]); |
Array
类型
创建数组的方法:
1 | var colors = new Array(); |
数组的长度保存在length
属性中
检测数组:Array.isArray(value)
调用数组的toString()
/valueOf()
方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。为了创建这个字符串会调用数组每一项的toString()
方法。
当调用数组的toLocaleString()
方法时,它也会创建一个数组值的以逗号分隔的字符串,但是调用的是每一项的toLocaleString()
方法。
使用join()
方法,则可以使用不同的分隔符来构建这个字符串。join()
方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。
push()
方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而pop()
方法则从数组末尾移除最后一项,减少数组的length
值,然后返回移除的项。
shift()
能够移除数组中的第一个项并返回该项,同时将数组长度减1。unshift()
能在数组前端添加任意个项并返回新数组的长度。
reverse()
方法会反转数组项的顺序。
在默认情况下,sort()
方法按升序排列数组项。sort()
方法会调用每个数组项的toString()
转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()
方法比较的也是字符串。sort()
方法可以接收一个比较函数作为参数。
1 | function compare(value1, value2) { |
concat()
方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。
slice()
方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice()
方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项—但不包括结束位置的项。注意,slice()
方法不会影响原始数组。参数也可以是负数。
splice()
方法可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。splice()
方法始终都会返回一个数组,该数组中包含从原始数组中删除的项。
indexOf()
和lastIndexOf()
方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。一个从前往后找,一个从后往前找。比较第一个参数和数组中的项时使用全等运算符。
每个迭代方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this
的值。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。
every()
:对数组中的每一项运行给定函数,如果该函数对每一项都返回true
,则返回true
。filter()
:对数组中的每一项运行给定函数,返回该函数会返回true
的项组成的数组。forEach()
:对数组中的每一项运行给定函数。这个方法没有返回值。map()
:对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。some()
:对数组中的每一项运行给定函数,如果该函数对任一项返回true
,则返回true
。
以上方法都不会修改数组中的包含的值。
reduce()
和reduceRight()
都会迭代数组的所有项,然后构建一个最终返回的值。其中,reduce()
方法从数组的第一项开始,逐个遍历到最后。而reduceRight()
则从数组的最后一项开始,向前遍历到第一项。
这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给reduce()
和reduceRight()
的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。
Date
类型
在调用Date
构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从UTC时间1970年1月1日午夜起至该日期止经过的毫秒数)。
Date.parse()
方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。如果传入Date.parse()
方法的字符串不能表示日期,那么它会返回NaN
。实际上,如果直接将表示日期的字符串传递给Date
构造函数,也会在后台调用Date.parse()
。
Date.UTC()
方法同样也返回表示日期的毫秒数,但它与Date.parse()
在构建值时使用不同的信息。Date.UTC()
的参数分别是年份、基于0的月份(一月是0,二月是1,以此类推)、月中的哪一天(1到31)、小时数(0到23)、分钟、秒以及毫秒数。在这些参数中,只有前两个参数(年和月)是必需的。
Date
构造函数也会模仿Date.UTC()
,但有一点明显不同:日期和时间都基于本地时区而非GMT
来创建。
Data.now()
方法返回表示调用这个方法时的日期和时间的毫秒数。
Date
类型的toLocaleString()
方法会按照与浏览器设置的地区相适应的格式返回日期和时间。这大致意味着时间格式中会包含AM或PM,但不会包含时区信息(当然,具体的格式会因浏览器而异)。而 toString()
方法则通常返回带有时区信息的日期和时间,其中时间一般以军用时间(即小时的范围是0到23)表示。
Date
类型的valueOf()
方法,则根本不返回字符串,而是返回日期的毫秒表示。
toDateString()
——以特定于实现的格式显示星期几、月、日和年;toTimeString()
——以特定于实现的格式显示时、分、秒和时区;toLocaleDateString()
——以特定于地区的格式显示星期几、月、日和年;toLocaleTimeString()
——以特定于实现的格式显示时、分、秒;toUTCString()
——以特定于实现的格式完整的UTC日期。
RegExp
类型
1 | var expression = / pattern / flags ; // 字面量形式 |
模式部分为正则表达式,标志部分为一个或多个标志:
g
:全局模式,正则表达式应用到所有字符串;i
:不区分大小写m
:多行模式
使用RegExp
构造函数时,元字符要双重转义。
RegExp
实例的属性lastIndex
:整数,表示开始搜索下一个匹配项的字符位置,从0算起。
方法exec()
接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回null
。 返回的数组包含两个额外的属性。其中,index
表示匹配项在字符串中的位置,而input
表示应用正则表达式的字符串。在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。
在不设置全局标志的情况下,在同一个字符串上多次调用exec()
将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用exec()
则都会在字符串中继续查找新匹配项。(lastIndex
属性会发生变化)
方法test()
测试是否匹配,返回一个布尔值。
长属性名 | 短属性名 | 说明 |
---|---|---|
input |
$_ |
最近一次要匹配的字符串。 |
lastMatch |
$& |
最近一次的匹配项。 |
lastParen |
$+ |
最近一次匹配的捕获组。 |
leftContext |
&\' |
input字符串中lastMatch之前的文本 |
multiline |
$* |
布尔值,表示是否所有表达式都使用多行模式。 |
rightContext |
$' |
Input字符串中lastMatch之后的文本 |
RegExp.$1
、RegExp.$2
…RegExp.$9
,分别用于存储第一、第二……第九个匹配的捕获组。
toLocaleString()
和toString()
方法会像它是以字面量形式创建的一样显示其字符串表示。
Function
类型
函数实际上是对象。每个函数都是Function
类型的实例,而且都与其他引用类型一样具有属性和方法。函数名实际上也是一个指向函数对象的指针。
函数声明:
1 | function sum (num1, num2) { |
函数表达式:
1 | var sum = function(num1, num2){ |
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
1 | alert(sum(10,10)); |
在代码开始执行之前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。
不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
arguments
对象的callee
属性指向拥有这个对象的指针。
1 | function factorial(num){ |
函数的this
属性引用的是函数据以执行的环境对象。全局作用域中是window
。caller
属性引用的是调用当前函数的函数,全局作用域中时null
。
函数的length
属性表示函数希望接收的命名参数的个数。
apply()
和call()
。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this
对象的值。首先,apply()
方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array
的实例,也可以是arguments
对象。call()
方法必须明确地传入每一个参数。
toLocaleString()
toString()
valueOf()
方法返回函数代码。
基本包装类型
每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。操作完成后基本包装类型的实例会立即被销毁。
Number
类型的toFixed()
方法返回按照指定小数位数的字符串表示,toExponential()
方法返回以给定小数位数的质数表示法的字符串表示,toPrecision()
方法会依情况返回上述二者中的一个。
String
类型的charAt()
方法返回指定位置的字符(也可以用方括号),charCodeAt()
方法返回给定位置字符的编码。
slice()
substring()
方法返回子串,第一个参数为开始位置,第二个参数为结束的后一个位置,substr()
类似,但是第二个参数是子串长度。
indexOf()
和lastIndexOf()
方法查找子串,分别是从前往后和从后往前,可以指定开始的位置。
trim()
方法删除字符前后缀的空格。
toLowerCase()
和toUpperCase()
转换大小写。
match()
方法接受正则表达式或者RegExp
对象,和RegExp
对象的exec()
方法一样。search()
方法类似,但是返回第一个匹配项的索引。replace()
方法接受两个参数:第一个参数可以是一个RegExp
对象或者一个字符串(只会转化匹配这个字符串的第一个子串),第二个参数可以是一个字符串或者一个函数。如果是字符串,可以用一些特殊的序列把匹配结果插入到字符串中。如果是函数,可以实现一些精细的替换操作。
split()
方法基于指定的分隔符把一个字符串分割成多个子串,分隔符可以是字符串或者RegExp
对象,可选的第二个参数指定数组的大小。
localeCompare()
方法比较该字符串和参数的字典序大小,如果大于参数,则返回正数。
String.fromCharCode()
方法把编码转化为字符串。
单体内置对象
不属于任何其他对象的属性和方法,最终都是Global
对象的属性和方法。
Global
对象的encodeURI()
和encodeURIComponent()
方法用于对URI编码,前者不会对本身属于URI的特殊字符编码,后者会对任何非标准字符编码。decodeURI()
和decodeURIComponent()
相反。
eval()
方法把传入的字符串参数当做JavaScript代码解析并且运行。在其中创建的任何变量和函数都不会被提升。
在全局作用域中声明的所有变量和函数,都成为了window
对象的属性。
Math
对象的一些属性:
属性 | 说明 |
---|---|
Math.E |
自然对数的底数,即常量e的值 |
Math.LN10 |
10的自然对数 |
Math.LN2 |
2的自然对数 |
Math.LOG2E |
以2为底e的对数 |
Math.LOG10E |
以10为底e的对数 |
Math.PI |
π的值 |
Math.SQRT1_2 |
1/2的平方根(即2的平方根的倒数) |
Math.SQRT2 |
2的平方根 |
Math.min()
和Math.max()
方法可以接受多个参数。
Math.ceil()
Math.floor()
Math.round()
方法执行舍入。
Math.random()
方法返回0到1之间的随机数。
第6章 面向对象的程序设计
理解对象
数据属性:
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Enumerable]]
:表示能否通过for-in
循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Writable]]
:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
。[[Value]]
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined
。
Object.defineProperty()
方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable
、enumerable
、writable
和value
。设置其中的一或多个值,可以修改对应的特性值。
1 | var person = {}; |
一旦把属性定义为不可配置的,就不能再把它变回可配置了。
在调用Object.defineProperty()
方法时, 如果不指定, configurable
、enumerable
和writable
特性的默认值都是false
。
访问器属性不包含数据值;它们包含一对儿getter
和setter
函数。 在读取访问器属性时,会调用getter
函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter
函数并传入新值,这个函数负责决定如何处理数据。
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性, 或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true
。[[Enumerable]]
:表示能否通过for-in
循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true
。[[Get]]
:在读取属性时调用的函数。默认值为undefined
。[[Set]]
:在写入属性时调用的函数。默认值为undefined
。
1 | var book = { |
定义多个属性:
1 | var book = {}; |
Object.getOwnPropertyDescriptor()
方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable
、enumerable
、get
和set
;如果是数据属性,这个对象的属性有configurable
、enumerable
、writable
和value
。
1 | var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); //2004 |
创建对象
构造函数:
1 | function Person(name, age, job){ |
1 | // 当作构造函数使用 |
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。通过把函数定义转移到构造函数外部来解决这个问题。
1 | function Person(name, age, job){ |
原型模式:每个函数都有一个prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
1 | function Person(){ |
创建了一个新函数时会为该函数创建一个prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor
(构造函数)属性,这个属性包含一个指向prototype
属性所在函数的指针。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针[[Prototype]]
(内部属性),指向构造函数的原型对象。如果[[Prototype]]
指向调用isPrototypeOf()
方法的对象,那么这个方法就返回true
,如下所示:
1 | alert(Person.prototype.isPrototypeOf(person1)); //true |
Object.getPrototypeOf()
方法返回[[Prototype]]
的值:
1 | alert(Object.getPrototypeOf(person1) == Person.prototype); //true |
读取某个对象的某个属性时,会先在对象实例本身搜索该属性,如果找不到的话再在原型对象中找该属性。不能通过对象实例重写原型中的值,给对象原型的属性赋值会把原型的值屏蔽。可以通过delete
操作符删除这个值来恢复访问原型中的值。
1 | function Person(){ |
使用hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回true
。
在单独使用时,in
操作符会在通 过对象能够访问给定属性时返回true
,无论该属性存在于实例中还是原型中。
在使用for-in
循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性, 也包括存在于原型中的属性。 屏蔽了原型中不可枚举属性(即将[[Enumerable]]
标记为false
的属性)的实例属性也会在for-in
循环中返回
Object.keys()
方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
使用Object.getOwnPropertyNames()
方法得到所有实例属性,无论它是否可枚举。
更简洁的原型语法:
1 | function Person(){ |
这种方法改变了Person
的constructor
属性,解决方案:
1 | Object.defineProperty(Person.prototype, "constructor", { |
可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。
但是如果重写原型对象,会创建新的原型对象,之前创建的实例的[[Prototype]]
指针还是指向原来的原型对象。
组合使用构造函数模式和原型模式:创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
1 | function Person(name, age, job){ |
动态原型模式:
1 | function Person(name, age, job){ |
使用动态原型模式时不要重写原型。
寄生构造函数模式:
1 | function SpecialArray(){ |
返回的对象和构造函数以及构造函数的原型之间没有关系。
稳妥构造函数模式:稳妥对象,指的是没有公共属性,而且其方法也不引用this
的对象。
1 | function Person(name, age, job){ |
使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系。
继承
原型链:
1 | function SuperType(){ |
SubType
继承了SuperType
,而继承是通过创建SuperType
的实例,并将该实例赋给SubType.prototype
实现的。实现的本质是重写原型对象,代之以一个新类型的实例。
instance.constructor
现在指向的是SuperType
。
调用instance.getSuperValue()
会经历三个搜索步骤:1)搜索实例;2)搜索SubType.prototype
; 3)搜索SuperType.prototype
,最后一步才会找到该方法。
给原型添加方法的代码一定要放在替换原型的语句之后。
通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。
在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
借用构造函数:
1 | function SuperType(){ |
借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
1 | function SuperType(name){ |
方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合继承:
1 | function SuperType(name){ |
原型式继承:
1 | function object(o){ |
1 | var person = { |
包含引用类型值的属性始终都会共享相应的值,person.friends
不仅属于person
所有,而且也会被anotherPerson
以及yetAnotherPerson
共享。实际上,这就相当于又创建了person
对象的两个副本。
Object.create()
方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()
与object()
方法的行为相同。
Object.create()
方法的第二个参数与Object.defineProperties()
方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
1 | var person = { |
寄生式继承:
1 | function createAnother(original){ |
寄生式继承不能做到函数复用。
组合继承中会调用两次超类型构造函数:
1 | function SuperType(name){ |
寄生组合式继承就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
1 | function inheritPrototype(subType, superType){ |
1 | function SuperType(name){ |
《JavaScript高级程序设计 第三版》读书笔记 - 第四至六章
https://blog.xqmmcqs.com/《JavaScript高级程序设计 第三版》读书笔记 - 第四至六章/