JS基础
# 基本概念
JS区分大小写;函数名不能适用
typeof
,它是一个关键字。标识符就是指变量、函数、属性的名字,或者函数的参数。
JS注释
// 单行注释 /** * 这是多行注释 * (块级)注释 */
严格模式:为
JavaScript
定义了一种不同的解析与执行模式。是ES5引入的一个概念,处理了ES3的一些不确定行为。整个脚本中使用或函数内部使用,分别加如下代码:// 整个脚本中,在顶部加 "use strict" // 函数内部 function doSomething() { "use strict" // 函数体 }
严格模式主要限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能适用
with
语句 - 不能对只读属性赋值,否则报错
- 不能适用前缀0表示八进制,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会再它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
和arguments.caller
- 禁止
this
指向全局对象 - 不能适用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字,如
protected
、static
、interface
尤其注意
this
的限制,ES6模块中,顶层的this
指向undefined
,即不应该在顶层代码中使用this
。JS语句以分号结尾,如果分号省略,则由解析器确定语句的结尾;建议任何时候都不要省略分号。
JS变量类型是松散类型的,即可以用来保存任何类型的数据,每个变量仅仅是一个用于保存值的占位符而已。
JS有5种基本数据类型:
Undefined
、Null
、Boolean
、Number
和String
;还有一种复杂的数据类型Object
,它本质上是由一组无序的名值对组成的。typeof
是操作符,而不是函数。它用来检测给定变量的数据类型。typeof
可能返回的字符串:- "undefined":如果这个值未定义;
- "boolean":如果这个值是布尔值;
- "string":如果这个值是字符串;
- "number":如果这个值是数值;
- "object":如果这个值是对象或
null
;null
会被认为是一个空的对象引用 - "function":如果这个值是函数。
Undefined
类型只有一个值,即undefined
。它是在第三版引入为了区分空对象指针与未经初始化的变量。初始声明的未赋值或赋值undefined
的变量是undefined
;但与尚未定义的变量是不同的。let message; // 下面变量未声明 // let age; alert(message); // “undefined” alert(age); // 产生错误 alert(typeof message); // “undefined” alert(typeof age); // “undefined”
然而,对未初始的变量执行
typeof
操作符会返回undefined
值,对未声明的变量进行此操作结果也是undefined
。Null
类型也只有一个值,即null
。从逻辑角度看,null
表示一个空对象指针,这也正式为什么使用typeof
操作符监测null值会返回"object"的原因。**如果定义的变量用于保存对象,那么最好将该变量初始化为null而不是其他值。**这样一来直接检查null
值就可以知道相应的变量是否已保存了一个对象的引用。也就是,只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存为null
。这样做不仅体现null
作为空对象指针的管理,而且有利于进一步区分null
和undefined
。Boolean
类型有两个字面值:true
和false
。可以对任何数据类型的值调用Boolean()
函数,而且总会返回一个Boolean
值,该值被保存在messageAsBoolean
变量中。Number
类型使用了IEEE754
格式来表示整数和浮点数。JS能够标识的最大数值保存在Number.MIN_VALUE
中,值是5e-324
;能够标识最大的值保存在Number.MAX_VALUE
中,值是1.7976931348623157e+308
。超出此范围的值会被转换为Infinity
和-Infinity
。可以使用isFinite()
函数,函数在参数位于最小与最大数值之间时会返回true
。NaN
是一个特殊的数值,即非数值(Not a Number),这个数值用于标识一个本来要返回数值的操作数未返回数值的情况。isNaN()
函数可以确定参数是否“不是数值”。NaN
有两个特点:- 任何涉及
NaN
的操作都会返回NaN
; NaN
与任何值都不相等,包括NaN
本身。
- 任何涉及
数值转换,有3个函数可以把非数值转换为数值:
Number()
、parseInt()
、parseFloat()
。String
类型用于表示由零或多个16位Unicode
字符组成的字符序列,即字符串。String
类型包含一些特殊的字符字面量,也叫转义序列,用于表示非打印字符或有其他用途的字符:\n \t \b \r \f \\ \' \"
。字符串是不可变的,字符串一旦创建,他们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量。
转换为字符串的方法,几乎每个值都有
toString()
方法,包括:数值、布尔值、对象、字符串。但null
和undefined
值没有这个方法。默认情况下,该方法以十进制返回字符串,同事支持二进制、八进制、十六进制,乃至其他任意有效进制格式表示的字符串值。var num = 10; alert(num.toString()); // "10" alert(num.toString(2)); // "1010" alert(num.toString(8)); // "12" alert(num.toString(10)); // "10" alert(num.toString(16)); // "a"
另一个方法是
String()
,这个函数能转换任何类型的值为字符串。遵循的转换规则:- 如果值有
toString()
方法,则调用该方法并返回相应结果; - 如果是
null
,则返回"null"; - 如果是
undefined
,则返回"undefined"。
- 如果值有
Object
类型是一组数据和功能的集合。它是所有它的实例的基础,即Object
类型所具有的任何属性和方法也同样存在于更具体的对象中。每个实例都具有下列的属性和方法:constructor
:构造函数,保存着创建当前对象的函数;hasOwnProperty(propertyName)
:用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在;isPrototypeOf(object)
:检查传入的对象是否是传入对象的原型;propertyIsEnumerable(propertyName)
:检查给定的属性是否能够用for-in
语句来枚举。toLocalString()
:返回对象的字符串表示,该字符串与执行环境的地区对应;toString()
:返回对象的字符串表示;valueOf()
:返回对象的字符串、数值或布尔值表示。通常与toString()
方法的返回值相同。
操作符用于操作数值:包括算数操作符、位操作符、关系操作符、相等操作符。
一元操作符:递增
++
、递减—
、加+
、减-
;位操作符:JS中的所有数值都是以IEEE-754 64位格式存储,但位操作符并不直接操作64位的值,而是先将64位的值转换成32位的整数,然后执行操作,最后再将结果转换回64位。对于有符整数,第32位表示符号,0 表示整数,1表示负数。负数使用的是二进制补码。
- 按位非
~
:返回数值的反码; - 按位与
&
:对应位都是1返回1,任何一位是0,结果返回0; - 按位或
|
:有一个位是1就返回1,只有两个位是0才返回0; - 按未异或
^
:只有一个1时才返回1,两个位都是1或都是0,返回0; - 左移
<<
:将数值的所有位向左移动指定的位数; - 有符右移
>>
:将数值向右移动,但保留符号位;在右移过程中会出现空位,空位在原数值的左侧、符号位右侧,此时会用符号位的值来填充所有空位; - 无符号右移
>>>
:将数值的所有32位向右移动;对正数来说,结果与有符右移相同。对于负数,无符号右移以0来填充空位,而不是像有符号右移以符号位的值来填充空位。无符右移操作符会把负数的二进制码当成正数的二进制码。而且,由于负数以其绝对值的二进制码补码形式存在,因此就会导致无符号右移后的结果非常之大。如:-64
的二进制码为11111111111111111111111111000000
,无符右移之后会把二进制码当成正数的二进制码,换算成十进制就是4294967232
,右移5位之后就成了00000111111111111111111111111110
即十进制134217726
。
- 按位非
布尔操作符:布尔操作符一共有3个:与
&&
、或||
、非!
;乘性操作符:乘法、除法、求模;
加性操作符:加法、减法;
关系操作符:小于、大于、小于等于、大于等于;
相等操作符:相等
==
和不相等!=
,先转换再比较;全等===
和不全等!==
,仅比较不转换,推荐使用。- 相等和不相等:先强制转型,再比较相等性
- 全等和不全等:两个操作数未经转换就相等的情况下返回
true
条件操作符:
variable = boolean_expression ? true_value : false_value;
赋值操作符:把右侧的值赋值给左侧的变量。下面是简化的复合赋值操作符,这些操作符的主要目的是简化赋值操作,不会带来任何性能的提升。
= *= /= %= += -= <<= >>= >>>=
逗号操作符:可以在一条语句中执行多个操作,如
let a = 1, b = 2, c = 3;
label
语句,使用label语句可以再代码中添加标签,以便将来使用:label: statement
。例子:定义start标签,可以再将来由break或continue语句引用。加标签的语句一般都要与for语句等循环语句进行配合使用。start: for(let i=0; i < count; i++){ console.log(i); }
函数参数:JS函数的参数不介意传进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个或三个,解析器都可以解析。因为JS中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过
arguments
对象来访问这个参数数组,从而获取传递函数的每个参数。没有传递值的命令参数将自动被赋予undefined
值,这就跟定义了变量但又没有初始化一样。函数没有重载:在其他语言中,可以为一个函数编写两个定义,只要两个定义的签名(接受的参数的类型和数量)不同即可。JS函数没有签名,因为参数是由包含零或多个值的数组来表示的。如果函数名字相同,之后定义的函数会覆盖先定义的函数。
function addSomeNumber(num){ return num + 100; } function addSomeNumber(num) { return num + 200; } const result = addSomeNumber(100); //300
# 变量
定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。但是对不同类型值进行操作是不一样的,对于引用类型我们可以为其添加属性和方法,也可以改变和删除其属性和方法。但是我们不能给基本类型添加属性或方法,尽管这样做不会导致错误。
复制变量值:如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。当一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。
传递参数:JS所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。在向参数传递基本类型值的值时,被传递的值会被复制给一个局部变量。在向参数传递引用类型时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
检测类型:
typeof
在检测基本数据类型时很有用,但在检测引用类型的值时,它的用处不大。通常,我们并不想知道某个值是对象,而是想知道它是什么类型的对象。JS提供了instanceof
操作符,语法如下:result = variable instanceof constructor
如果变量是给定引用类型的实例,那么
instanceof
操作符就会返回true
。根据规定,所有引用类型的值都是Object
的实例。因此在检测一个引用类型值和Object
构造函数时,instanceof
操作符始终会返回true
。检测基本类型的值,会返回false
,因为基本类型不是对象。// 变量person是Object吗 alert(person instanceof Object); // 变量colors是Array吗 alert(colors instanceof Array); alert(patterninstanceofRegExp);
# 引用类型
引用类型的值(对象)是引用类型的一个实例。在JavaScript中引用类型是一种数据结构,用于将数据和功能组织在一起。它也常被称为类,但这种称呼并不妥当。
创建Object实例的方式有两种。一种是使用
new Object()
;另一种是使用对象字面量表示法。const person = new Object(); person.name = "Nicholas"; person.age = 29; const person = { name : "Nicholas", age : 29 };
在使用对象字面量语法时,属性名也可以使用字符串,但这里的数值属性名会自动转换为字符串:
const person = { "name" : "Nicholas", "age" : 29, 5 : true };
访问对象属性时可以使用点表示法或方括号表示法。方括号表示法的有点事可以通过变量来访问属性。
JS数组的每一项可以用来保存任何类型的数据。数组的大小可以动态调整,即可以随着数据的添加自动增长以容纳新增数据。
Array.isArray()
可以用来检测数据是不是数组。所有对象都具有
toLocalString()
、toString()
、valueOf()
方法。toString()
方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。Array的方法:
push()
:在数组末端添加任意个项,并返回新数组的长度pop()
:移除数组中的最后一项,并返回此值shift()
:移除数组中的第一个项,并返回此值unshift()
:在数组前端添加任意个项,并返回新数组的长度reverse()
:数组倒序排列,返回排序后的数组sort()
:按升序排列数组项;或者接收一个函数,进行定制排序设计;返回排序后的数组concat()
:合并两个或多个数组,并返回新数组slice()
:splice
:主要用途是向数组中部插入项,有三种使用方法:- 删除:可以删除任意数量的项,只需指定2个参数,要删除的第一项的位置和要删除的项数。如:splice(0,2)会删除数组中的前两项。
- 插入:可以向指定位置插入任意数量的项,主需要提供3个参数:起始位置、0(要删除的项数)和要插入的项。如果插入多个项,可以再传入第四、第五,以至任意多个项。如:splice(2,0,"red", "green")会从当前数组的位置2开始插入字符串"red"和"green"。
- 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需要指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。如:splice(2,1,"red","green")会删除当前数组位置2的项,然后再从位置2开始插入字符串"red"和"green"。
indexOf()
和lastIndexOf
:这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。分别是:从数组的开头开始向后查找;从数组末尾开始向前查找;都返回查找的项在数组中的位置,或者在没有找到的情况下返回-1。- 迭代方法
every()
:对数组中每一项运行给定函数,如果该函数对每一项都返回true
,则返回true
filter
:对数组中每一项运行给定函数,返回该函数会返回true
的项组成的数组。forEach()
:对数组中每一项运行给定函数。无返回值。map()
:对数组中每一项运行给定函数,返回每次调用的结构组成的数组。some()
:对数组中每一项运行给定函数,如果该函数对任一项返回true
,则返回true
。- 归并方法
reduce()
和reduceRight()
:两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中,reduce()
方法从数组的第一项开始,逐个遍历到最后。而reduceRigth()
从数组追后一项开始,向前遍历到第一项。都接受两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。函数接受四个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。
JS中的
Date
类型使用自UTC
时间。Date
类型重写了toLocalString()
、toString()
和valueOf()
方法。valueOf()
方法直接返回日期的毫秒表示。常用的日期方法:
getTime()
:返回日期的毫秒数getFullYear()
:取得4位数的年份getMonth()
:返回日期中的月份,从0开始,0表示1月getDate()
:返回日期月份中的天数(1到31)getDay()
:返回星期中的星期几,从0开始getHours()
:返回日期中的小时数getMinutes()
:返回日期中的分钟数(0到59)getSeconds()
:返回日期中的秒数,超过59会增加分钟数
RegExp类型
在JS中函数实际上是对象。
Function
函数的方式是使用构造函数,可以接收任意数量的参数,但最后一个参数始终都被看成函数体,而前面的参数则枚举出了新函数的参数。下面的例子:let sum = new Function("num1", "num2", "return num1 + num2");
从技术角度讲,这是一个函数表达式。但是,不推荐使用这种方法定义函数,因为这种语法导致解析两次代码(第一次是解析床柜JS代码,第二次是解析传入构造函数中的字符串),产品那个人影响性能。但是这种语法可以让人很直观地理解“函数是对象,函数名是指针”的概念。
由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同,即一个函数可能有多个名字。使用不带括号的函数名是访问函数指针,而非调用函数。
function sum(num1, num2){
return num1 + num2;
}
alert(sum(10,10)); //20
var anotherSum = sum;
alert(anotherSum(10,10)); //20
sum = null;
alert(anotherSum(10,10)); //20
此时,anotherSum
和sum
就指向了同一个函数,调用snotherSum()
也可以被调用并返回结果。
深入理解函数没有重载:将函数名想象为指针,上面提到的函数名相同后面的会覆盖前面的。而利用指针概念:
let addSomeNumber = function (num){ return num + 100; }; addSomeNumber = function (num) { 6 return num + 200; }; const result = addSomeNumber(100); //300
重写代码后,创建的第二个函数实际上覆盖了引用第一个函数的变量
addSomeNumber
。函数声明与函数表达式:解析器在执行环境中加载数据时,对函数声明,解析器会率先读取,并使其在执行任何代码之前可用;置于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行。
因为JS的函数名本身就是变量,所以函数也可以作为值来使用。不仅可以当做参数,也可以当做值来返回。
函数内部有两个特殊的对象:
arguments
和this
。arguments.caller
该属性是一个指针,指向保存着调用当前函数的函数的引用;arguments.callee
也是一个指针,指向拥有这个arguments
对象的函数。每个函数都包含两个属性:
length
和prototype
。length
表示函数接收的命名参数的个数。prototype
属性是不可枚举的。每个函数包含三个非继承而来的方法:
apply()
和call()
。作用:在特定的作用域中调用函数,实际上等于设置函数体内this
对象的值。bind()
:会创建一个函数的实例,其this
值会被绑定到传给bind()
函数的值。为了便于操作基本类型值,JS还提供了3个特殊的引用类型:
Boolean
、Number
和String
。每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。- 创建String类型的一个实例;
- 在实例上调用指定的方法;
- 销毁这个实例
可以把这三个步骤想象成执行以下代码:
let s1 = "some text"; let s2 = s1.substring(2); s1 = null;
引用类型和基本包装类型的主要区别是对象的生存期。使用
new
操作符创建的引用类型的实例,在执行流离开当前作用于之前都一直保存在内存中。自动创建的基本包装类型对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。var s1 = "some text"; s1.color = "red"; alert(s1.color); //undefined
第二行创建的String对象在执行第三行代码时已经被销毁了;第三行又创建了自己的String对象,而该对象没有
color
属性。Number
是与数字值对应的引用类型。Number类型重写了valueOf()
、toLocaleString()
和toString()
方法,重写后valueOf()
返回对象表示的基本类型的数值,另外两个函数则返回字符串形式的数值。toString()
将数值转为字符串,默认是10进制;toFixed()
方法会按照指定的小数位返回数值的字符串表示。let num1 = 10; console.log(num1.toFixed(2)); // "10.00" let num2 = 10.005; console.log(num.toFixed(2)); // "10.01"
String
类型是字符串的对象包装类型。这个类型中有很多方法帮助字符串进行解析和操作:字符方法
charAt()
和charCodeAt()
:接收一个基于0的字符位置。第一个函数返回给定位置的那个字符。第二个函数返回给定位置的那个字符的字符编码。字符串操作方法:
concat()
:将一个或多个字符串拼接起来。可以用加号操作符或字符串模板代替。slice
、substr()
和substring()
这三个方法都会返回被操作字符串的一个子字符串,而且也接收一个或两个参数。第一个参数指定字符串的开始位置,第二个参数表示子字符串到哪里结束。具体来说,slice()
和substring()
的第二个参数指定的是子字符串最后一个字符后面的位置。而substr()
的第二个参数指定的则是返回的字符个数。这些函数都是不会修改字符串本身的值,只是返回一个基本类型的字符串值。在给这些方法传递负数的情况下,slice()
会将传入的负值与字符串的长度相加,substr()
方法将负的第一个参数加上字符串长度,而将负的第二个参数转换为0,substring()
会把所有的负数参数都转为0。字符串位置方法
indexOf()
和lastIndexOf()
:从一个字符串中搜索给定的子字符串,然后返会子字符串的位置,没找到则返回-1。区别是:前者是从字符串开头向后搜索子字符串,而后者是从字符串的末尾向前搜索子字符串。都有第二个参数,表示从字符串的哪个位置开始搜索。trim()
:创建一个字符串副本,删除前置及后缀的所有空格,然后返回结果。字符串大小写转换方法:
toLowercase()
、toUpperCase()
。字符串模糊匹配方法
match()
、search()
、replace()
localeCompare()
:比较两个字符串,字符串比参数字符串在字母表之前返回-1,相同返回0,之后返回1。var stringValue = "yellow"; alert(stringValue.localeCompare("brick")); //1 alert(stringValue.localeCompare("yellow")); //0 alert(stringValue.localeCompare("zoo")); //-1
fromCharCode()
:接收一或多个字符编码,然后将它们转换成一个字符串。String.fromCharCode(104, 101, 108, 108, 111); // "hello"
内置对象的定义是:由JS实现提供的、不依赖于宿主环境的对象,这些对象在JS程序执行之前就已经存在了。即在使用时开发人员不必显示地实例化内置对象,因为它们已经实例化了。内置对象:
Object
、Array
、String
。JS还定义了两个单体内置对象Global
、Math
。Global
对象是一个很特别的对象,不管从什么角度看,这个对象都是不存在的。JS中的Global
对象:不属于任何其他对象的属性和方法,最终都是它的属性和方法。事实上没有全局变量或全局函数;所有在全局作用域中定义的属性和函数,都是Global
对象的属性。诸如isNan()
、isFinite()
、parseInt()
以及parseFloat()
实际上全都是Global
对象的方法。Global
对象的其他方法:encodeURI()
、encodeURIComopnent()
可以对URI(Uniform Resource Identifiers,通用资源标识符)进行编码,它们用特殊的UTF-8编码替换所有无效的字符,从而让浏览器能够接受和理解。encodeURI()
主要用于整个URI,encodeURIComopnent()
用于对URI的某一段进行编码。区别在于encodeURI()
不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而encodeURIComponent()
则会对它发现的任何非标准字符进行编码。第二个使用更多,因为实践中更常见的是对查询字符串参数而不是基础URI进行编码。let uri = "http://www.wrox.com/illegal value.htm#start"; //"http://www.wrox.com/illegal%20value.htm#start" console.log(encodeURI(uri)); //"http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start" console.log(encodeURIComponent(uri));
decodeURI()
、decodeURIComponent()
,第一个只对encodeURI()
替换的字符进行编码。第二个可以解码任何特殊字符的编码。let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start"; //http%3A%2F%2Fwww.wrox.com%2Fillegal value.htm%23start console.log(decodeURI(uri)); //http://www.wrox.com/illegal value.htm#start console.log(decodeURIComponent(uri));
eval()
:将传入的参数当做实际的JS语句来解析,然后把执行结果插入到原位置。Global
对象的属性:
Math对象的属性:大都是数学计算中可能用到的一些特殊值。
Math对象的方法:
min()
、max()
确定一组数之中的最小值和最大值。舍入方法:
ceil()
:向上舍入;floor()
向下舍入;round()
:四舍五入random()
:返回大于等于0小于1的一个随机数其他方法:
# 面向对象的程序设计
ECMA-262
把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数,即对象是一组没有特定顺序的值。数据属性和访问器属性:数据属性包含一个数据值的位置;访问器属性包含一对
getter()
和setter()
函数,不过这两个函数都不是必须的。创建对象的几种模式:
工厂模式:
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { alert(this.name); }; return o; } let person1 = createPerson("Nicholas", 29, "Software Engineer"); let person2 = createPerson("Greg", 27, "Doctor");
函数
createPerson()
能根据接受的参数来构建一个包含所有必要信息的Person
对象。此模式虽然解决了创建多个相似对象的问题,但没有解决对象的识别问题即怎样知道一个对象的类型。构造函数模式:JS的构造函数可用来创建特定类型的对象,如Object和Array这样的原生构造函数。也可以创建自定义构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { alert(this.name); }; } let person1 = new Person("Nicholas", 29, "Software Engineer"); let person2 = new Person("Greg", 27, "Doctor");
与工厂模式不同的是:没有显示地创建对象;直接将属性和方法赋给了
this
对象;没有return
语句;函数名Person
使用的是大写字母P,主要为了区别JS中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。这种方式调用构造函数会经过以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象,因此this就指向了这个新对象;
- 执行构造函数中的代码,为这个对象添加属性;
- 返回新对象。
这两个对象都有
constructor
属性,该属性指向Person
。console.log(person1.constructor == Person); // true console.log(person2.constructor == Person); // true
构造函数和普通函数没区别,通过new操作符调用就可以作为构造函数,不通过new调用,就是普通函数。
使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。前面的
person1
、person2
都有一个名为sayName()
的方法,但这两个方法不是同一个Function
的实例。因为JS中函数是对象,所以每定义一个函数,也就实例化了一个对象。从逻辑上讲,此时的构造函数也可以这样定义:function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = new Function("alert(this.name)"); }
这样看,更容易看明白,每个
Person
实例都包含一个不同的Function
实例。也就是以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function
新实例的机制仍然相同。因此不同实例上的同名函数是不相等的。创建两个完成同样任务的Function
实例没有必要,且有this
对象在,不用再代码执行前就把函数绑定到特定对象上面。可以把函数定义转移到构造函数外部来解决这个问题。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, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
这样确实解决了这个问题,但是新的问题出现了。在全局作用域中定义的函数只能被某个对象调用,而且如果对象需要定义许多方法,那么就要定义很多个全局函数,那么我们自定义的引用类型就没有封装性可言了。
原型模式:创建的每个函数都有一个
prototype
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思理解,
prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。function Person() { } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; const person1 = new Person(); person1.sayName(); //"Nicholas" const person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName == person2.sayName); //true
与构造函数模式不同的是,新对象的这些属性和方法由所有实例共享。
person1
和person2
访问的都是同一组属性和同一个sayName()
函数。组合使用构造函数模式和原型模式:这是最常用的一种方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外这种模式还支持向构造函数中传递参数,可谓集两者之长。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); // false alert(person1.sayName === person2.sayName); // true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性
constructor
和方法sayName()
则是在原型中定义的。而修改了personl.friends
(向其中添加一个新字符串),并不会影响到person2.friends
,因为它们分别引用了不同的数组。 这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。动态原型模式:把所有信息封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。也就是说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job){ // 属性 this.name = name; this.age = age; this.job = job; // 方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ console.log(this.name); }; } } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName();
仅在
sayName()
不存在的情况下,才会添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,完成初始化后不需要再做什么改修了。请记住,不能使用对象字面量重写原型,重写之后会切断现有实例与新原型之间的联系。寄生构造模式:基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新对象的创建;表面上看,很像是典型的狗在函数。
function Person(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); }; return o; } var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName(); //"Nicholas"
这个模式可以在特殊的情况下用来为对象创建构造函数。如创建一个具有额外方法的特殊数组。由于不能直接修改构造函数,可以使用这个模式。
function SpecialArray(){ // 创建数组 var values = new Array(); // 添加值 values.push.apply(values, arguments); // 添加方法 values.toPipedString = function(){ return this.join("|"); }; // 返回数组 return values; } var colors = new SpecialArray("red", "blue", "green"); console.log(colors.toPipedString()); // "red|blue|green"
有一点需要说明:首先,返回的对象与构造函数或者构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖
instanceof
操作符来确定对象类型。由于上述存在的问题,建议使用其他模式,不要用这种模式。靠稳妥构造函数模式:首先要提到稳妥对象,指的是没有公共属性,而且其方法也不引用
this
的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this
和new
),或者放置数据被其他应用程序改动时使用。
function Person(name, age, job){
// 创建要返回的对象
var o = new Object();
// 在这里定义私有变量和函数
// 添加方法
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
这样,变量friend
中保存的是一个稳妥对象,而除了调用sayName ()
方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传 人到构造函数中的原始数据。
# 函数表达式
定义函数的方式有两种:一种是函数声明,另一种是函数表达式。关于函数声明,一个重要的特征就是函数声明提升,即在执行代码之前会先读取函数声明。关于函数表达式,形式好像常规的变量赋值语句,即创建一个函数并将它赋值给变量,这种情况下创建的函数叫匿名函数,又叫拉姆达函数。
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别:
// 错误 函数声明提升 if(condition){ function sayHi() { console.log("Hi!"); } } else { function sayHi() { console.log("Yo!"); } } // 匿名函数 let sayHi; if(condition){ sayHi = function() { console.log("Hi!"); } } else { sayHi = function() { console.log("Yo!"); } }
递归函数:在一个函数通过名字调用自身的情况下构成递归函数。
function factorial(num) { if(num <= 1) { return 1; } else { return num * factorial(num-1); } }
这是一个经典的递归阶乘函数,虽然表面看着没有问题,但是下面代码可能出错:
let anotherFactorial = factorial; factorial = null; console.log(anotherFactorial(4));
以上代码先把
factorial()
函数保存到anotherFactorial
中,再将factorial
设置为null
,结果指向原始函数的引用只剩下一个。但在接下来的调用中,函数内部必须执行factorial()
,而factorial
已经不再是函数,所以就会导致出错。这时
arguments.callee
可以解决这个问题,它是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用。function factorial(num) { if(num <= 1) { return 1; } else { return num * arguments.callee(num-1); } }
这样可以保证无论怎样调用函数都不会出现问题,因此使用
arguments.callee
代替匿名函数更为保险。但是在严格模式下,访问这个属性会导致错误。不过,可以使用匿名函数达到相同的效果:
let factorial = (function f(num){ if(num <= 1){ return 1; } else { return num * f(num-1); } });
以上代码创建一个名为
f()
的函数命令表达式,然后将它赋值给factorial
,这样几遍把函数赋值给另一个变量,函数的名字仍然有效,所以递归调用照样能正确完成。这种严格模式和非严格模式下都行得通。闭包函数:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数:
function createComparisonFunction(propertyName) { return function(object1, object2) { let value1 = object1[propertyName]; let value2 = object2[propertyName]; if(value1 < value2) { return -1; } else if(value1 > value2) { return 1; } else { return 0; } }; }
value1
和value2
两行代码时内部函数(一个匿名函数)中的代码,这梁行代码访问了外部函数中的变量propertyName
。即使这个内部函数被放回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName
。之所以还能访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction()
的作用域。要搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。