js由浅入深的红宝书。
注:本文章所列内容是根据《javascript高级程序设计》一书的总结。只包含本人应该要了解或记住的或之前未了解过的,可能并不包含所有内容。
1 html
中的javascript
1.1 script元素
script
元素主要包含属性:
-
async
:表示应该立即开始下载脚本,但不能阻止下载资源或等待其他脚本加载,只对外部脚本有效。 -
defer
: 表示脚本可以延迟到文档完全解析和显示内容之后再执行,只对外部文件有效。再IE7以及更早的版本中,对行内脚本也可以指定这个属性(但是会被忽略)。 -
crossorigin
: 配置CORS(跨源资源共享)设置,默认不使CORS。当其值为anonymous
时,配置文件请求不必包含凭据标志。当其值为use-credentials
设置凭据标志,意味着出站请求会包含凭据。 -
integrity
: 允许比对接收到的资源和指定的加密签名以验证子资源的完整性(SRI, Subresponse Integrity)。如果接收到的签名与这个指定的签名不匹配,则页面会报错,脚本不会执行。可以用于确保CDN(内容分发网络:Content Delivery Nerwork)不会提供而已内容。 -
type
: 代替language, 表示脚本中语言的内容类型(也称MIME类型)。按照惯例这个值始终为text/javascript
, 尽管text/javascript
和text/ecmascript
都已经废弃。js的MIME类型通常都是application/x-javascript
,不过设置这个值有可能导致脚本被忽略。在非IE的浏览器中还可以设置application/javascript
和application/ecmascript
。 如果这个值为module
,则代码会被当成ES6模块,并且只有此时才可以使用import和export关键字。
在行内使用js代码时,代码中不能出现字符串</script>
。例如下面的代码会导致报错。
<script>
function say() {
console.log("</script>")
}
</script>
浏览器对行内脚本的解析方式决定了他在看到字符串</script>
会将其当成结束的</script>
标签。想要避免这个问题需要使用转义字符\
即可。
<script>
function say() {
console.log("<\/script>")
}
</script>
注:按照惯例js文件扩展名为.js
这不是必须的,因为浏览器不会检查所包含js文件的扩展名。这就为使用服务器端脚本语言动态生成js代码,或者在浏览器中将js的扩展语言(如ts
或jsx
)转译为js提供了可能性。不过,服务器会经常根据文件扩展来确定响应的正确MIME类型,如果不用.js
扩展名,一定要确保服务器能返回正确的MIME类型。
另外,如果使用src
属性,script
标签内不应该在包含其他js代码。如果两者同时提供的话,浏览器指挥下载并执行脚本文件,从而忽略行内文件。
如果有scr
属性,浏览器会像该属性指定的链接发起一个get
请求,以取得相应资源。这个get请求不受浏览器的同源策略限制,但返回并执行的文件则受限制。当然这个请求仍然收父页面http/https
协议的限制(注:这个限制具体指什么?有哪位大佬能告知下)。
1.1.1 延迟执行脚本
当为script
设置defer
属性,会告诉浏览器立即开始下载,但延迟执行。也就是说脚本会被延迟到整个页面解析完毕后(即:解析到</html>
标签后)再运行。
注:html5
规范要求当多个script
标签都设置了defer
,脚本应该按顺序执行,而且两者都会再DomContentLoaded
事件执行之前开始执行。但是在实际中,推迟的脚本并不一定总会按照顺序执DomContentLoaded
事件执行之前完成,因此最好只包含一个这样的脚本。defer
属性也只对外部脚本有效,这是html5
中明确规定的,因此支持html5
的浏览器都会忽略行内脚本的defer属性。
1.1.2 异步执行脚本
当为script
标签设置async
属性时,也会告诉浏览器开始立即下载。
不过,与defer
不同的是,标记为async
脚本并不能按照他们出现的顺序依次执行。
同时,异步脚本保证会在页面的load
事件前执行,但可能会在DomContentLoaded
事件之之前或之后执行。使用async
也会告诉页面你不会使用documnet.write
,不过最好的Web开发实践并不推荐使用这个方法, 如果使用则会出现下面警告。
1.1.3 动态创建脚本
let script = document.createElement('script');
script.src = 'a.js'
document.head.appendChild(script);
在把HTMLElement
元素添加到DOM之前,浏览器不会发起请求。默认情况下,以这种方式创建的script
元素是以异步方式加载的,相当于添加了async
属性。但是并不是所有浏览器都支持async
属性,如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:
let script = document.createElement('script');
script.src = 'a.js'
script.async = false
document.head.appendChild(script);
以这种方式获取的资源对浏览器的预加载器是不可见的。这回这会严重影响它们在资源获取队列中的优先级。这种方式可能会严重影响性能。要想让预加载器知道这些动态请求的文件存在,可以在文档头部显示声明它们:
<link rel="preload" href="a.js">
1.1.5 async与defer对比
借用网上的一张图片,如下(parser:解析,net: 下载,execution: 执行)
1.1.6 XHTML中的变化
XHTML
是将HTML
做为XML
的应用重新包装的结果,在XHTML
中js必须指定type属性且值text/javasctipt
,HTML
中则没有这个属性。XHTML
虽然已经退出历史舞台,但实践中可能会遇到遗留代码,所以需要了解下。
XHTML
编写规则比HTML
中严格。如下代码,在HTML
中有效,但是XHTML
中无效。
<script type="text/javascript">
function compare(a, b) {
if(a < b) {
console.log('a is less than b')
}else if (a > b) {
console.log('a is greater that b')
}else {
console.log('a is equal to b')
}
}
</script>
在HTML
中,解析到<script>
标签会应用特殊规则,但XHTML
中则没有这些规则,这意味着a < b
中的<
会被解析为一个标签的开始。
解决有两种方法,第一种是将所有<
号换成实体形式<
<script type="text/javascript">
function compare(a, b) {
if(a < b) {
console.log('a is less than b')
}else if (a > b) {
console.log('a is greater that b')
}else {
console.log('a is equal to b')
}
}
</script>
第二种是将所有代码包含CDATA
块中,CDATA
块表示文档中可以包含任意文本的区块,其内容不会作为标签来解析。
<script type="text/javascript"><![CDATA[
function compare(a, b) {
if(a < b) {
console.log('a is less than b')
}else if (a > b) {
console.log('a is greater that b')
}else {
console.log('a is equal to b')
}
}
]]</script>
在兼容XHTML
的浏览器,这可以解决问题,但在不支持CDATA
块的非XHTML
兼容浏览器中则不行。为此,需要用js注释来抵消。下面这种格式适用所有浏览器。
<script type="text/javascript">
//<![CDATA[
function compare(a, b) {
if(a < b) {
console.log('a is less than b')
}else if (a > b) {
console.log('a is greater that b')
}else {
console.log('a is equal to b')
}
}
//]]
</script>
1.2 行内代码与外部文件
外部文件具有可维护性,缓存,适应未来(通过把js文件放到外部文件中,包含外部js文件的语法在HTML
和XHTML
中是一样的)
在初次请求时,如果浏览器支持SPDY/HTTP2
,就可以从同一个地方取得一批文件,并将它们逐个放到浏览器中缓冲中。从浏览器的角度看,通过SPDY/HTTP2
获取所有这些独立的资源与获取一个大的js文件延迟差不多。
注: 当然不支持SPDY/HTTP2
的浏览器还是一个大文件合适。
1.3 文档模式
IE5.5发明了文档模式的概念,最初的文档主要有混杂模式和标准模式,前者让IE有一些非标准的特性,后者让其具有兼容标准的行为。虽然这两种模式主要区别只体现在通过CSS渲染的内容方面,但对js也有一些关联影响。
IE初次支持文档模式切换以后,其他浏览器也跟着实现了。所以又出现第三种模式准标准模式,没有标准模式规定的那么严格。主要区别体现在如何对待图片元素周围的空白(在表格中使用图片时最明显)。
混杂模式没有doctype
声明,并且在不同浏览器中实现差别很大。
准标准模式与标准模式非常接近,很少区分。人们在说标准模式时,可能指其中任何一个。而对文档模式的检测也不会区分它们,后面所说的标准模式就是除了混杂模式之外的模式。
1.4 <noscript>
下面两种情况下之一,都会显示包含在<noscript>
中的内容:
- 浏览器不支持脚本
- 浏览器对脚本的支持被关闭
2 语言基础
2.1 语句
js语句以分号结尾,省略分号意味着由解析器确定语句在哪里结尾。即使分号不是必须的,也应该加上,这有助于防止省略造成的问题。加分号也便于开发者通过删除空行来压缩代码(如果没有分号,只删除空行,会报语法错误)。加分号某些情况下会提升性能,因为解析器会尝试在合适的地方补上分号来纠正错误。
2.2 变量
2.1 var
关键字
- 使用
var
声明的关键字会有变量提升。 - 如果省略
var
关键字,直接定义变量,则该变量会定义为全局变量。 var
作用域只有全局和函数作用域,如果在全局声明变量,该变量会变为window
上的属性。
2.2 let
关键字
let
声明范围是块作用域。而var
声明的是函数作用域。let
变量不会在作用域中被提升。- js引擎在解析代码时,会注意出现块后面的
let
声明。在let
声明之前执行的瞬间叫暂时性死区,因此在此阶段引用任何后面才声明的变量,都会抛出ReferenceError
。 let
声明的全局变量不会成为window
的属性。
2.3 const
关键字
const
与let
行为基本相同,只不过是作为常量,不能修改。如果声明的是一个对象,则可以修改对象的属性。
注:最佳实践,不使用var
, const
优先, let
次之。
2.4 数据类型
原始数据类型:String
,Boolean
,Number
,Null
,Undefined
,Symbol
复杂数据类型:Object
-
typeof
操作符返回值为
string
,boolean
,number
,symbol
,undefined
,object
(对象或null
),function
注:当在
Safari
(直到Safari 5
)和Chrome
(直到Chrome 7
)中用于检测正则表达式时,由于实现细节的原因,typeof
会返回function
。ECMA-262
规定,任何实现内部[[call]]
方法的对象都应该在typeof
检测时返回function
。因为上述浏览器中实现了这个方法,所以在调typeof
时,返回function
。在IE
和Firefox
中,typeof
对正则表达式返回object
。 -
Null
类型逻辑上将,
null
表示一个空对象的指针,这也是typeof null
为object
的原因。 -
Number
类型整数可以用八进制和十六进制字面量表示。对于八进制字面量,第一个数字必须是
0
,然后是相应的八进制数0-7
,如果字面量超出了应有的范围,就会忽略前缀的0
,后面的数被当成十进制数。let num1 = 070 // 有效的八进制值,相当与十进制的56 let num2 = 08 // 无效的八进制值,表示十进制的8
要创建十六进制字面量,必须让真正的数值前缀为
0x
(区分大小写)
注:使用八进制还是十六进制创建的数值在所有的数学操作中都被视为十进制的值。
console.log(num1 + 1) // 57
浮点型:因为存储浮点值使用的内存空间是存储整数值的两倍,所以ECMASCRIPT
总是想方设法把值转换为整数,在小数点后面没有数字的情况下,数值就会变为整数。默认情况下,ECMASCRIPT
会将小数点后至少包含6
个零的浮点值转换为科学计数法(例:0.0000001
会变成e-7
)。浮点值的精确度最高可达17位,但在算数中远不如整数精确。(例:0.1 + 0.2 = 0.300000000000000004
)
值的范围:js可以表示最小数值保存在Number.MIN_VALUE
中,最大数值保存在Number.MAX_VALUE
中,如果数值超出了范围,会自动将其值变为Infinity
和-Infinity
。检测是否是有限值的方法为isFinite
。
NaN:意思是Not a Number
。其他语言中用0
除任意数值都会导致错误,从而终止代码,在js中0
,+0
,-0
相除会返回NaN
,如果分子是非0
,则返回Infinity
或-Infinity
。
console.log(0/0); // NaN
console.log(-0/+0); // NaN
console.log(1/0); // Infinity
console.log(1/-0); // -Infinity
NaN
与任何值计算,始终为NaN
,并且NaN
不等于包括NaN
在内的任何值。判断是一个数值是否为NaN
,可以用isNaN
方法。
注:isNaN
可用于测试对象,此时,会首先调用对象的valueOf()
方法,确定返回的值是否可以转换为数值。如果不能,再调用toString()
方法,并测试其返回值。
数值转换:有三种方法Number()
,parseInt()
,parseFloat()
, Number()
方法基于如下规则:
- 布尔值,将
true
变为1
,false
变为0
。 - 数值,直接返回。
null
,返回0
。undefined
,返回NaN
- 字符串,如果字符串全部为有效的整数,则返回该整数(例:
Number(011)
返回11
);如果字符串全部为有效的浮点数,则返回相应的浮点数(同样,忽略前面的0
);如果字符串包含又凶啊的十六进制数值,则会返回对应的十进制数值;空字符串会返回0
;如果字符串包含上述情况之外的字符,则返回NaN
。 - 对象,先调用
valueOf()
方法,并按照上述规则进行转换,如果结果是NaN
,则会调用toString()
方法,再按上述规则转换。
parseInt()
更专注于字符串是否包含数值模式。
- 字符串最前的空格会被忽略,从第一个非空字符串开始转换,如果第一个字符不是字符、加号或减号。会返回
NaN
。这以为这空字符串会返回也会返回NaN
(这点和Number()
不一样)。 - 如果第一个非空字符串是数值字符、加号或减号,则继续检测每个字符,知道字符串末尾或非数值字符。(例:
parseInt(' 1sun')
会变为1
。) - 如果第一个字符是
0x
开头,就会被按照16进制的数解析。 - 如果第一个字符是
0
,则会按照八进制进行解析(严格模式下或新版本浏览器还是按照十进制解析。)。
console.log(parseInt('010')) // IE8中输出8 // IE9及其以上,现代浏览器如Chrome中输出10, parseInt(010)结果为8,因为010本身就是表示八进制的数值。
- 也可以传递第二参数进行手动指定-底数(进制数)。(例:
parseInt(10, 2)
为2
按照二进制解析)。不传 底数相当于让其自己决定如何解析,所以为避免出错,应始终传第二个参数。
parseFloat()
工作方式和parseInt()
工作方式类似,(例:parseFloat(2.3.2)
结果为2.3
)。但是它始终忽略字符串开头的0
。这意味这十六进制数值始终返回0
。
String
类型
字符串是不可以变的,一旦创建了,它们的值就不能变了。字符串赋值整个过程是,首先创建一个能容纳结果字符串的空间,然后填充上字符,最后销毁之前的字符串。这也是早期浏览器拼接字符串非常慢的原因。这些浏览器在后来的版本中针对性的解决了这个问题。
toString()
方法 几乎所有值都有这个方法。null
和undefined
没有。当数值调用这个方法时可以传递一个参数,即以什么底数来输出该字符串。
let num = 10
console.log(num.toString(2)) // '1010'
如果你不确定一个值是null
或undefined
,可以使用String()
函数,它遵循:如果该值有toString()
方法,则调用该方法(不传参数),并返回值;如果是null
,则返回'null'
,如果是undefined
则返回'undefined'
。
- 模板字面量的标签函数
标签函数会接收被插值记号被分隔后的模板和对每个表达式求值的结果。标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为。例如:
let a = 1;
let b = 2;
function tag(strings, a, b, c) {
console.log(strings); // ["", " + ", " = ", ""]
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}
tag`${a} + ${b} = ${a+b}`
上面的tag
函数也可以使用剩余操作符。
function tag(strings, ...expressions) {
console.log(strings)
for(const e of expressions) {
console.log(e)
}
}
- 原始字符串
使用模板字面量可以直接获得原始模板字面量的内容(如换行符),而不是被转换后的字符表示。例如
console.log(`1\n2`)
// 1
// 2
console.log(String.raw`1\n2`) // 结果是 "1\n2"
// 对于实际的换行符来说是不行的。它们不会被转换成转义序列的形式。
console.log(String.raw`1
2`)
// 1
// 2
另外也可以通过标签函数第一个参数,即字符串数组的.raw
属性取得每个字符串的原始值。
function printRaw(strings) {
for(const str of strings) {
console.log(str)
}
for(const str of strings.raw) {
console.log(str)
}
}
printRaw`\u00A9`
// ©
// \u00A9
Symbol
类型
符号是原始值,且符号实例是唯一的,不可变的。符号的用途是确保对象的属性是唯一的,就是用来创建唯一记号,来用做非字符串形式的对象属性。
let s1 = Symbol(),
s2 = Symbol(),
s3 = Symbol('foo'), // 也可以传入一个字符串描述
s4 = Symbol('foo')
console.log(s1 == s2) // false
console.log(s3 == s4) // false
Symbol()
函数不能与new
关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象。
-
全局符号注册表
如果想需要重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此需要使用
Symbol.for()
方法。使用某个符号调用时,它会检查全局注册表,如果没有与传入的字符串匹配的符号,则会创建,如果有,直接返回改符号实例。
let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
let s3 = Symbol('foo')
console.log(s1 === s2) // true
console.log(s2 === s3) // false
全局注册表中的符号必须用字符串键来创建,因此Symbol.for()
接收的任何值都会转换成字符串。
console.log(Symbol.for()) // Symbol(undefined)
还可以使用Symbol.keyFor()
来查询全局注册表,该方法接收符号作为参数,返回对应的字符串键。如果查询不是全局符号,则返回undefined
。如果传入的参数不是符号,则抛出TypeError
错误。
let s = Symbol.for('foo')
console.log(Symbol.keyFor(s)) // foo
let s2 = Symbol('bar')
console.log(Symbol.keyFor(s2)) // undefined
console.log(Symbol.keyFor(1)) // TypeError: 1 is not a symbol
- 凡是可以使用字符串和数值作为属性的地方,都可以使用符号。
let s = Symbol()
let obj = {[s]: 'obj'} // 第一种
obj[s] = 'obj' // 第二种
// 第三种方式
Object.defineProperty(obj, s, {value: 'obj'})
Object.getOwnPropertyNames()
返回对象的常规数组,Object.getOwnPropertySymbols()
返回对象的符号属性数组,二者互斥。
如果没有显示的保存符号属性的引用,则必须通过遍历对象的所有符号属性才可以找到相应的键。
let obj = {
[Symbol('foo')]: 'foo',
[Symbol('bar')]: 'bar'
}
let sKey = Object.getOwnPropertySymbols(obj).find(symbol => symbol.toString().match(/foo/))
console.log(sKey) // Symbol(bar)
-
常用内置符号:
es6
引入了常用的内置符号,用于暴露语言内部行为,开发者可以直接访问,重写,或模拟这些行为。这些内置符号都以Symbol
工厂函数字符串属性的形式存在。Symbol.asyncIterator
: 作为一个属性表示一个方法,该方法返回对象默认的AsyncIteator
,由for-await-of
方法使用。for-await-of
执行异步迭代操作。循环时,它们会调用Symbol.asyncIterator
为键的函数,并期望返回一个实现迭代器API
的对象(通常是实现该API
的AsyncGenerator
)
class Emitter { constructor(max) { this.max = max this.index = 0 } async *[Symbol.asyncIterator]() { while(this.index < this.max) { yield new Promise(resolve => resolve(this.index++)) } } } async function count() { let e = new Emitter(3) for await(const x of e) { console.log(x) } } count() // 0, 1, 2
Symbol.hasInstance
: 作为一个属性,表示一个方法,该方法来判断传入的实例是否是当前构造函数的实例,由instanceof
操作符使用。
class Foo {} let f = new Foo() console.log(f instanceof Foo) // true console.log(Foo[Symbol.hasInstance](f)) // true class Parent {} class Son extends Parent { static [Symbol.hasInstance]() { return false } } let son = new Son() console.log(Parent[Symbol.hasInstance](son)) // true console.log(son instanceof Parent) // true console.log(Son[Symbol.hasInstance](son)) // false console.log(son instanceof Son) // false
Symbol.isConcatSpreadable
:作为一个属性,表示一个布尔值,如果是true
,则意味着对象应该用Array.prototype.concat()
打平其数组元素。数组对象默认会被打平到已有的数组,false
或假值会导致整个数组被追加到已有的数组中;类数组对象默认情况下,会被追加到数组末尾,true
或真值会让类数组被打平到已有数组。
let initArr = [1] let otherArr = [2] console.log(otherArr[Symbol.isConcatSpreadable]) // undefined console.log(initArr.concat(otherArr)) // [1,2] otherArr[Symbol.isConcatSpreadable] = false console.log(initArr.concat(otherArr)) // [1, [2]] let arrLike = {length: 1, 0: 2} console.log(arrLike[Symbol.isConcatSpreadable]) // undefined console.log(initArr.concat(arrLike)) // [1, {length: 1, 0: 2}] arrLike[Symbol.isConcatSpreadable] = true console.log(initArr.concat(arrLike)) // [1,2]
Symbol.iterator
: 作为一个属性表示一个方法,该方法返回默认的迭代器。由for-of
使用。换句话说,这个符号表示实现迭代器API
的函数。
class Emitter { constructor(val) { this.max = val this.index = 0 } *[Symbol.iterator]() { while(this.index < this.max) { yield this.index++ } } } function count() { let e = new Emitter(3) for(const x of e) { console.log(x) } } count() // 1, 2, 3
Symbol.match
:作为一个属性表示一个正则表达式方法,该方法用正则表达式去匹配字符串,由String.prototype.match()
方法使用。String.prototype.match()
方法会使用以Symbol.match
为键的函数来对正则表达式求值,正则表达的原型上默认有这个函数的定义,给这个方法传入非正则表达式会导致该值被转换成RegExp
对象。Symbol.match
函数接收一个参数,就是调用match()
方法的字符串实例。
class Matcher { static [Symbol.match](target) { return target.includes('foo') } } console.log('foobar'.match(Matcher)) // true console.log('bar'.match(Matcher)) // false class Matcher { constructor(str) { this.str = str } [Symbol.match](target) { return target.includes(this.str) } } console.log('foobar'.match(new Matcher('foo'))) // true console.log('baz'.match(new Matcher('foo'))) // false
Symbol.replace
作为一个属性表示一个正则表达式方法,该方法替换一个字符串中匹配的子串,由String.prototype.replace()
方法使用。String.prototype.replace()
方法会使用以Symbol.replace
为键的函数来对正则表达式求值。正则表达的原型上默认有这个函数的定义。Symbol.replace
接收两个参数,即调用replace()
方法的字符串和替换的字符串。返回值没有限制。
class Replacer { static [Symbol.replace](target, str) { return target.split('foo').join(str) } } console.log('foobar'.replace(Replacer, 'baz')) // 'bazbar' class Replacer { constructor(str) { this.str = str } [Symbol.replace](target, str) { return target.split(this.str).join(str) } } console.log('foobar'.replace(new Replacer('foo'), 'baz')) // 'bazbar'
Symbol.search
:作为一个属性,表示一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()
方法使用。String.prototype.search()
方法会使用以Symbol.search
为键的函数来对正则表达式求值,正则表达的原型上默认有这个函数的定义。Symbol.search
函数接收一个参数,就是调用search()
的字符串。
class Searcher { static [Symbol.search](target) { return target.indexOf('foo') } } console.log('foobar'.search(Searcher)) // 0 class Searcher { constructor(str) { this.str = str } [Symbol.search](target) { return target.indexOf(this.str) } } console.log('foobar'.search(new Searcher('foo'))) // 0
Symbol.species
作为一个属性表示一个函数值,该函数作为创建派生对象的构造函数。这个属性在内置类中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用Symbol.sepcies
定义静态的获取器(getter
)方法,可以覆盖新创建实例的原型定义:
class Foo extends Array {} let foo = new Foo() console.log(foo instanceof Array) // true console.log(foo instanceof Foo) // true foo = foo.concat('bar') console.log(foo instanceof Array) // true console.log(foo instanceof Foo) // true class Bar extends Array { static get [Symbol.species]() { return Array } } let bar = new Bar() console.log(bar instanceof Array) // true console.log(bar instanceof Bar) // true bar = bar.concat('bar') console.log(bar instanceof Array) // true console.log(bar instanceof Bar) // false
Symbol.split
: 作为一个属性,表示一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串,由String.prototype.split()
方法使用。String.prototype.split()
方法会以Symbol.split
为键的函数来对正则表达式求值。正则表达式原型上默认有这个函数的定义。Symbol.split
方法接收一个参数,就是调用split()
方法的字符串。
class Splitor { static [Symbol.split](target) { return target.split('foo') } } console.log('1foo2'.split(Splitor)) class Splitor { constructor(str) { this.str = str } [Symbol.split](target) { return target.split(this.str) } } console.log('1foo2'.split(new Splitor('foo')))
Symbol.toPrimitive
:作为一个属性,表示一个方法,该方法将对象转换为相应的原始值,由ToPrimitive
抽象操作使用。很多内置操作都会尝试强制将对象转换为原始值,包括字符串,数值和未指定的原始类型。根据提供给这个函数的参数(string
,number
,default
),可以控制返回的原始值:
class Foo {} let foo = new Foo() console.log(3 + foo) // "3[object Object]" console.log(3 - foo) // NaN console.log(String(foo)) // "[object]" class Foo { [Symbol.toPrimitive](hint) { switch (hint) { case "number": return 3; case "string": return "string foo" case "default": default: return "default foo" } } } let foo = new Foo() console.log(3 + foo) // "3default foo" console.log(3 - foo) // 0 console.log(String(foo)) // "string foo"
Symbol.toStringTag
作为一个属性表示一个字符串,该字符串用于创建对象的默认字符串描述,由Object.prototype.toString()
方法使用。通过toString()
方法获取对象的标识时,会检索由Symbol.toStringTag
指定的实例标识符,默认为"Object"。
let s = new Set() console.log(s); // Set(0) {} console.log(s.toString()) // "[object Set]" console.log(s[Symbol.toStringTag]) "Set" class Foo {} let foo = new Foo() console.log(foo) // Foo {} console.log(foo.toString()) // "[object Object]" console.log(foo[Symbol.toStringTag]) // undefined class Foo { [Symbol.toStringTag] = 'foo' } let foo = new Foo() console.log(foo) // Foo {} console.log(foo.toString()) // "[object foo]" console.log(foo[Symbol.toStringTag]) // "foo"
Symbol.unscopables
:作为一个属性,表示一个对象,该对象所有的以及继承的属性,都会从关联对象的with
环境中排除。这个属性对应的键值为true
,就可以阻止该属性出现在with
环境绑定中:
let o = {foo: 'bar'} with(0) { console.log(foo) // bar } o[Symbol.unscopeables] = { foo: true } with(o) { console.log(foo) // ReferenceError }
Object
类型
创建对象的几种方式
let obj = {}
let obj2 = new Object()
let obj3 = new Object // 如果没有参数,可以这么写,合法,但不推荐。
每个对象都有下面几种方法:
constructor
: 用于创建当前对象的函数hasOwnProperty(propertyName)
: 用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性必须为字符串或符号。isPrototypeOf(object)
: 用于判断当前对象是否是传入对象的原型。propertyIsEnumerable(propertyName)
: 用于判断给定的属性是否可以使用for-in
语句枚举,与hasOwnProperty()
一样,属性名必须为字符串或符号。toLocaleSTring()
: 返回对象的字符串表示,该字符串反应对象所在的本地化执行环境。toString()
: 返回对象的字符串表示。valueOf()
: 返回对象对应的字符串,数值或布尔值表示,通常与toString()
的返回值相同
注:ECMAScript
中Object
是所有对象的基类,所以任何对象都有这些属性和方法,但是,严格来讲,ECMA-262
中对象的行为不一定适合js中的其他对象。比如浏览器环境中的BOM
和DOM
,都是由宿主环境定义和提供的宿主对象。而宿主对象不收ECMA-262
约束,所以它们可能会也可能不会继承Object
。
2.5 操作符
- 递增/递减操作符(
--
,++
)
let a = 1
a-- // 相当于 a = a - 1
--a // 相当于 a = a - 1
// 区别
let a = 1, b = 1
let aa = --a + 1 // 先计算a = a - 1,再 + 1, 然后将值赋值给aa
let bb = b-- + 1 // 先计算b + 1, 将值赋值给bb, 然后在执行 b = b - 1
console.log(a, aa) // 0, 1
console.log(b, bb) // 0, 2
如果不是数值类型,则基于如下规则
- 字符串:有效的数值形式,则将其转换为数值在应用改变。
- 字符串:无效的数值形式,则将变量的值设置
NaN
。 - 布尔值:
false
变为0
,true
变为1
再进行计算。 - 对象:则调用它的
valueOf()
方法取得可以操作的值。然后应用上述规则,如果是NaN
则调用toString()
并再次应用其他规则。
+
,-
如果将其用于非数值,则会执行与使用Number()
一样的类型转换: 布尔值false
和true
会相应变为0
和1
,对象则会调用他们的valueOf()
或toString()
方法得到转换的值。
- 位操作
js中所有数值都以IEEE 754
64
位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32为整数,在进行位操作,之后再把结果转为64位。对开发者而言,就好像是只有32位整数一样。
有符号整数使用的是32位的前31位表示整数值,第32位表示符号,0
为正,1
为负。
负值是以2补数
(或补码)的二进制编码存储,一个数值的二补数通过找到数值补数(或反码),然后加1得到。
- 非
~
它的作用是返回数值的补码。
- 与
&
第一个数值的位 | 第二个数值的位 | 结果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
- 或
|
第一个数值的位 | 第二个数值的位 | 结果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
- 异或
^
第一个数值的位 | 第二个数值的位 | 结果 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
- 左移
<<
左移会以0
填充这些空位。
let a = 2 // 等于二进制10
let b = a << 5 // 等于二进制1000000 即64
- 有符号右移
>>
右移实际是左移的逆运算。左移和右移都会保持符号位。
let a = 64 // 等于二进制1000000
let b = a >> 5 // 等于二进制10 即2
- 无符号右移
>>>
对于正数,与有符号右移结果相同 对于负数,无符号右移操作符将负数的二进制当成正数的二进制来表示,因为负数是其绝对值的二补数,所以右移之后结果变的非常大。
let a = -64 // 11111111111111111111111111000000
let b = a >>> 5 // 134217729
- 逻辑操作符
-
逻辑非
!
,其遵循以下规则:
- 如果操作数是对象,非空字符串,非
0
数值(包括Infinity
),则返回false
- 空字符串,数值
0
,null
,NaN
,undefined
返回true
- 如果操作数是对象,非空字符串,非
-
逻辑与
&&
let a = true && false // 如果左边为true,则返回右面的值,如果左边为false,则返回左边的值。
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循以下规则:
第一个操作数是对象,则返回第二个操作数。
第一个操作数是null
,undefined
,NaN
,''
,0
则返回第一个操作数。
- 逻辑或
||
let a = true || false // 如果左边为true,直接返回左面的值,如果左边为false,才返回右面的值
与逻辑与类似,如果操作数不是布尔值,则逻辑或并不一定会返回布尔值,而是遵循以下规则:
第一个操作数是对象,则返回第一个操作数。
第一个操作数是null
,undefined
,NaN
,''
,0
则返回第二个操作数。
- 乘性操作符
-
*
- 如果任意操作数
NaN
,则返回NaN
- 如果
Infinity
乘0
,则返回NaN
- 如果
Infinity
乘非0
的有限值,则根据第二个操作数返回Infinity
或-Infinity
- 如果
Infinity
乘Infinity
,则返回Infinity
- 如果有不是数值的操作数,则现在后台调用
Number()
转为数值,然后再应用上述规则。
- 如果任意操作数
-
/
- 如果任意操作数为
NaN
,则返回NaN
- 如果
Infinity
除Infinity
,则返回NaN
- 如果
0
除0
,则返回NaN
- 如果非
0
的有限值除以0
,则根据第一个操作数返回Infinity
或-Infinity
- 如果
Infinity
除以任何有限值,则根据第二个操作数返回Infinity
或-Infinity
- 如果有不是数值的操作数,则现在后台调用
Number()
转为数值,然后在应用上述规则。
- 如果任意操作数为
-
%
- 如果
Infinity
除有限值,则返回NaN
- 如果有限值除
0
,则返回NaN
- 如果是
Infinity
除Infinity
,则返回NaN
- 如果是有限值除
Infinity
,则返回该有限值 - 如果是
0
除非0
,则返回0
- 如果有不是数值的操作数,则现在后台调用
Number()
转为数值,然后在应用上述规则。
- 如果
+
infinity
加-infinity
为NaN
- 如果其中有一个操作数为字符串,则会将另一个操作符转为字符串,然后拼接在一起。
- 如果有操作数是对象,则会调用
toString()
方法转为字符串,如果是null
或undefined
则会调用String()
将其变为'null'
或'undefined'
,然后进行字符的拼接。
-
infinity
减infinity
为NaN
- 如果任意操作是字符串,布尔值,
null
,undefined
,在后台调用Number()
将其转为数值,然后在进行运算。 - 如果有操作数是对象,则调用其
valueOf()
方法取得他的数值,如果没有该方法,则调用toString()
方法,然后再将字符串转为数值。
- 比较操作符
<
>
>=
<=
- 如果操作数都是字符串,则会比较字符串对应的编码
- 如果任一操作数是数值,则将另一个字符转为数值,然后比较。
- 如果任一操作数是对象,则调用其
valueOf()
,取得结果后然后应用以上规则,如没有valueOf()
,则调用toString()
然后在应用以上规则。 - 如果任一操作数是布尔,则可以将其转为数值在比较。
- 相等操作符
==
和!=
- 如果任一操作数是布尔,则将其转为数值,再进行比较
- 如果操作数是字符串和数值,则会将字符串变为数值,然后在比较。
- 如果一个操作数是对象,另外一个不是对象,则尝试调用对象的
valueOf()
方法取得其原始值,再根据前面的规则比较。 null
和undefined
相等,并且不能转换其他类型的值再比较。NaN
和任何操作数都不等,包括它本身。- 如果两个都是对象,则比较他们是不是引用同一个对象
- 全等
===
和!==
全等操作符不会进行类型转换,如果类型不一致,肯定是不等。
2.6 表达式
for
for(initialization, expression, post-loop-expression) statement // 接收初始化语句,表达式,循环后表达式,这几个都是不是必须的。
for(;;) { // 无限循环
doSomething()
}
// 如果只包含表达式,实际上就变成了while循环
let count = 10;
let i = 0;
for(; i < count; ) {
console.log(i);
i ++;
}
for-in
for(prop in object) statement
for(const prop in window) { // 注:const不是必须的,但为了确保这个局部变量不被修改,推荐使用`const`
console.log(prop)
}
该语句不能保证返回对象属性的顺序, 会因浏览器的实现而不同。如果循环迭代的变量是null
或undefind
,则不执行循环体(即:for(const i in null){console.log(i)}
)。
- 标签语句
通过在语句的前面加上标签
start: for(let i = 0; i < count; i ++) {
console.log(i)
}
在这个例子中,start
是一个标签,可以在后面通过break
或continue
语句中使用,标签语句的典型应用场景就是嵌套循环。
let num = 0;
outermost:
for(let i = 0; i < 10; i ++) {
for(let j = 0; j < 10; j ++) {
if(i == 5 && j == 5) {
break outermost;
}
num ++
}
}
console.log(num) // 55
switch
语句
为避免不必要的条件判断,最好给每个条件后面都加上break
语句。如果确实需要连续匹配多个条件,那么推荐写个注释表示是故意忽略了break
switch(i) {
case 25:
case 35:
// 匹配25或35时
console.log(i);
break;
case 45:
console.log(45);
break;
default:
console.log('other value')
}
3 变量、作用域与内存
3.1 原始值与引用值
原始值就是Undefined
,Null
,Boolean
,Number
,String
和Symbol
,原始值是按值访问的,我们操作的就是存储在变量中的实际值。
而引用值是保存在内存中的对象。与其他语言不同,js不允许直接访问内存的位置。在操作对象时,实际操作的是对象的引用而非对象本身。保存引用值的变量是按引用
访问的。
3.1.1 动态属性
对象可以随意添加属性,但是原始值没有属性,尽管尝试给原始值添加属性不会报错,比如:
let name = 'bill'
name.age = 18
console.log(name.age) // undefined
代码想给name
添加一个age
属性并赋值为18
,紧接着下一行属性不见了。原因详见**原始值包装对象**
3.1.2 复制值
原始值复制是复制值本身,即前后两个值是独立的。
但是引用值复制的实际上是一个指针,它指向存储在堆内存中的对象。因此操作完成后,两个变量实际上指的是同一个对象。
let obj1 = {}
let obj2 = obj1
obj1.name = 'bill'
console.log(obj2.name) // bill
3.1.3 参数传递
原始值和引用值的作为参数传递时,其实就是复制相应的原始值和引用值。因此,如果直接将参数复制给另外一个变量时,并不会影响和改变外面的变量。但是如果是给对象的添加,修改某个属性时,则外面的属性也会发生相应变化。
let obj = {}, num = 1
function test(obj, num) {
obj = {name: 'bill'}
num = 2
}
test(obj, num)
console.log(obj, num) // {} 1
function addProp(obj) {
obj.name = 'bill'
}
addProp(obj)
console.log(obj) // {name: "bill"}
3.2 执行上下文与作用域
变量和函数的执行上下文决定了他们可以访问哪些数据,以及他们的行为。每个上下文都有个关联的变量对象,而这个上下文中定义的函数和变量都存在于这个对象上。
全局上下文是最外成的上下文。在浏览器中,全局上下文就是window
对象,因此所有通过var
定义的全局变量和函数都会产能为window
对象的属性和方法。使用let
和const
的顶级声明不会定义在全局上下稳重,但在作用域链解析上效果是一样的。
上下文在代码执行的时候,会创建变量对象的作用域链。这个作用域链决定了各级上下文中代码在访问变量和函数时的顺序。
每个函数都有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。函数执行完以后,上下文栈会弹出该函数的上下文,将控制权返回给之前的上下文。上下文在其所有代码执行完毕以后会销毁。
注:访问局部变量比访问全局变量要快,因为不用切换作用域,不过js引擎在优化标识符查找上做了很多工作,将来这个差异可能就微不足道。
3.2.1 作用域链增强
执行上下文主要有全局上下文和函数上下文两种(eval()
调用内部存在第三种上下文),但 有其他方式来增加作用域链,某些语句会导致作用域链前端临时添加一个上下文,这个上下文在代码执行后会被销毁。在下面任意一种情况是会出现这个现象。
try/catch
语句的catch
块with
语句
对于with
语句来说,会向作用域链前端增加指定的对象,对于catch
语句而言,则会创建一个新的变量对象,这个对象会包含要抛出的错误对象的声明。
注:IE的实现在IE8之前是有偏差的,即它们会将catch
语句中捕获到的错误添加到执行上下文的变量对象上,而不是catch
语句的变量对象上,导致在catch
块外部都可以访问到错误,IE9纠正了这个问题。
3.3 垃圾回收
在C和C++等语言中,跟踪内存使用对开发者来说是一个很大的负担,也是很多问题的来源。js为开发者写下这个负担,通过自动内存管理实现内存分配和闲置资源回收。其思路是:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一段时间就会自动运行。
垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量主要有两种标记策略:标记清理和引用计数。
3.3.1 标记清理
当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放他们的内存,因为上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开的标记。
给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另外一个列表。标记过程的实现并不重要,关键在于策略。
IE
,FirFox
,Chrome
,Opera
和Safari
都在自己的js实现中采用标记清理或其变体,只是在运行垃圾回收的频率上有所差异。
3.3.2 引用计数
其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用次数为1。如果这个值又被赋值给其他变量,则引用值加1,类似的,如果这个变量被其他值覆盖了,那么引用数减1。当一个值的引用次数为0的时候,就说明没有办法在访问这个值了,因此可以安全的收回其内存。
引用计数最早由Netscape Navigator 3.0
采用,但很快就遇到严重的问题:循环引用。
function Problem() {
let objA = {}
let objB = {}
objA.otherObj = objB
objB.otherObj = objA
}
在上面的例子中,objA
和objB
通过各自的属性相互引用,意味着它们的引用数都是2。在标记清理的情况下,这不是问题,因为在函数结束以后,这两个对象都不在作用域中。而在引用计数的策略下,objA
和objB
在函数运行结束以后还会存在,因为它们的引用计数永远不会为0。如果函数被多次调用,则会导致大量的内存永远不会被释放。
在IE8以及更早版本的IE中,并非所有的对象都是原生js对象。BOM
和DOM
中的对象时C++实现的组件对象模型(COM
,Component Object Model Model)对象,而COM
对象使用引用计数来实现垃圾回收。因此,即使这些IE版本的js引擎使用标记清理,js存取的COM
对象依旧使用的是引用计数。换句话说,只要涉及COM
对象,就无法避开循环引用问题。
let el = document.getElementById('id')
let obj = {}
obj.el = el
el.obj = obj
这个例子DOM
对象和原生对象之间制造了循环引用。因此DOM
元素的内存永远不会被回收,即使它已经从页面删除了也是如此。为了避免这个问题,应该确保页面不使用的情况下切断原生js对象和DOM
元素之间的连接。比如:
obj.el = null
el.obj = null
把变量设置为null
实际上会切断变量和引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。
为了补救这一点,IE9把BOM
和DOM
对象改成了js对象。同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏的现象。
3.3.3 性能
垃圾回收程序会周期的运行,如果内存中分配了很多变量,则可能会造成性能损失,因此垃圾回收程序的调度很重要。尤其是运行在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。因此最好的办法在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。
现代垃圾回收程序会基于对js运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。比如V8团队2016年的一篇博文:在一次完整的垃圾回收之后,V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。
由于调度垃圾回收程序方面的问题导致性能下降,IE曾饱受诟病。它的策略是根据分配数,比如分配了256个变量、4096个对象/数组字面量和数组槽位(slot
),或者64KB字符串。主要满足其中某个条件,垃圾回收程序就会运行。问题在于,分配那么多变量的脚本很可能在其整个生命周期内都需要那么多变量,结果就会导致垃圾回收程序过于频繁的运行。因此,IE7最终更新了垃圾回收程序。
IE7发布后,js引擎被调优为动态分配变量,字面量或数组槽位等会触发垃圾回收的阈值。IE7的起始阈值和IE6相同。若垃圾回收程序回收的内存不到已分配的15%,这些变量,字面量,或数据槽位的阈值就会翻倍。如果有一次垃圾回收的内存已达到分配的85%,则阈值重置为默认值。这么一个简单的修改,极大的提升了重度依赖js得网页在页面中的性能。
注:在某些浏览器中是有可能(但不推荐)主动触发垃圾回收的,在IE中,window.CollectGarbage()
方法会立即触发垃圾回收。在Opera 7
及更高版本中,调用window.opera.collect()
也会启动垃圾回收程序。
3.3.4 内存管理
js运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少的多。分配给移动浏览器的就更少了。这更多是出于安全的目的,就是为了避免运行大量js的网页耗尽系统内存而导致操作系统崩溃。这个内存不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。
将内存占用量保持在一个较小的数值可以让页面更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,请把它设置为null
,从而释放其引用。这个最适合全局变量和全局对象的属性。
注:解除一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不再上下文里了,因此它在下次垃圾回收时会被回收。
- 通过
const
和let
声明来提升性能
这两个关键字不仅有助于改变代码风格,还有助于改进垃圾回收的进程。因为二者都以块为作用域,所以相比var
,使用这两个关键字可能会更早的让垃圾回收程序接入,尽早收回相应的内存。在块作用域比函数作用域更早终止的情况下,就有可能发生。
- 隐藏类和删除操作
根据js所在的运行环境,需要根据浏览器使用的js引擎来采取不同的性能优化策略。现如今,Chrome
是最流行的浏览器,使用V8 Javascript
引擎。V8在将解释js代码编译为实际的机器码时会利用隐藏类。如果你的代码非常注重性能,那么这一点可能对你很重要。
能够共享相同的隐藏类的对象性能会更好,V8会针对这种情况进行优化,但不一定能总是做到。比如:
function Article() {
this.title = 'hhh'
}
let a1 = new Article()
let a2 = new Article()
V8会在后台配置,这两个实例共享相同的隐藏类。但是假设之后又添加了下面的代码:
a2.author = 'Bill'
此时两个实例就会对应不同的隐藏类。根据这种操作频率和隐藏类的大小,这有可能会对性能产生明显影响。
解决方案就是避免js 先创建再补充式 的动态属性赋值,并在构造函数中一次声明所有属性:
function Article(author) {
this.title = 'jjj'
this.author = author
}
let a1 = new Article()
let a2 = new Article('Sun')
这样两个实例就基本一样了,可以共享一个隐藏类,从而带来潜在的性能提升。不过要记住,使用delete
关键字会导致生成不同的隐藏类片段。即使两个实例使用同一构造函数。动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性值设为null
,这既可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。
delete a2.author
- 内存泄漏
- 意外声明的全局变量是最常见但也是最容易修复的内存泄漏问题。
function setName() {
name = 'Jake' // 此时解释器会把该变量当做window的属性
}
- 定时器
let name = 'Jake'
setInterval(()=> { // 只要定时器一直运行,回调函数中的name就会一直占用内存。
console.log(name)
}, 1000)
- 闭包
let outer = function() {
let name = 'Jake'
return function() {
return name
}
}
调用outer()
就会导致分配给name
的内存被泄漏。
- 静态分配与对象池
为了提升js性能最后要考虑的一点往往就是压榨浏览器了,此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但是可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那么就可以保住因释放内存而损失的性能。
浏览器决定何时运行垃圾回收程序的一个标准就是对象的更替速度,如果很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采取更为激进的方式调度垃圾回收程序运行,这样当然会影响性能。例如:
function addVector(a, b) {
let result = new Vector()
result.x = a.x + b.x;
result.y = a.y + b.y;
return result
}
调用这个函数时,会在堆上创建一个新对象,然后将其返回。如果这个对象的生命周期很短,那么它会很快失去对它的引用,成为可以被回收的值。如果这个函数被频繁的调用,那么垃圾回收程序发现该对象更替速度快,就会频繁的安排垃圾回收。解决的方法是:不要动态创建对象,而是使用已有的对象:
function addVector(a, b, result) {
result.x = a.x + b.x;
result.y = a.y + b.y;
return result
}
当然,这需要在其他地方创建result
,那么在哪里创建它可以不让垃圾回收调度程序盯上呢?一个策略就是使用对象池。在初始化某一时刻,可以创建对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象,设置其属性,使用它,然后在操作完成后再把它还给对象池。
如果对象池只按需分配对象(对象不存在时创建新的,存在时复用),那么这个实现的本质是贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构来维护所有对象,数组是一个比较好的选择。不过使用数组实现,必须注意不要招致额外的垃圾回收。比如:
let resultList = new Array(100)
let result = new Vector()
resultList.push(result)
由于js数组的大小是动态可变的,当调用push()
方法时,引擎会删除大小为100的数组,再创建一个新的大小为200的数组。垃圾回收程序会看到这个删除操作,可能很快就会跑来收一次垃圾。要避免这种动态分配操作,可以在初始化时就创建一个足够大的数组,从而避免上述先删除后创建的操作。
注:静态分配是优化的一种形式,如果你的应用被垃圾回收严重地拖了后腿,可以利用它来提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不必在意。
4 基本引用类型
4.1 Date
Date
类型将日期保存为自协调世界时(UTC
, Universal Time Coordinated
)时间1970年1月1日零时至今所经过的好描述。使用这种存储格式,Date
类型可以精确表示1970年1月1日之前以及之后285616年的日期。
let date = new Date() // 不传参默认是当前的日期和时间, 参数可以其他日期时间距1970年1月1日零时的毫秒数
4.1.1 Date.parse()
该方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫米数。支持下列日期格式:
- “月/日/年”,如"5/1/2020"
- “月名 日,年”,如"May 1, 2020"
- “周几 月名 日 年 时:分:秒 时区”,如"Sat May 1 2020 00:00:00 GMT-0800";
- ISO 8601扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如"2020-05-01T00:00:00"(只适用于兼容ES5的实现)
如果直接把表示日期的字符串传给Date
构造函数,那么Date
会在后台调用Date.parse()
,下面的代码是到等价的。
let date = new Date(Date.parse("May 1, 2020"))
let sameDate = new Date("May 1, 2020")
如果传给Date.parse()
的字符串并不表示日期,则该方法返回NaN
4.1.2 Date.UTC()
该方法也返回日期的毫秒表示。参数是年、零起点月数(0表示1月)、日(1-31)、时(0-23)、分、秒和毫秒。其中前两个参数年和月时必传的。如果不提供日,则默认为1日。
let date = new Date(Date.UTC(2000,0)) // Sat Jan 01 2000 08:00:00 GMT+0800 (中国标准时间)
与Date.parse()
和Date.UTC()
也会被Date
构造函数隐式调用。不过有一个区别:这种情况下创建的是本地的日期:
new Date(2000,0) //Sat Jan 01 2000 00:00:00 GMT+0800 (中国标准时间) 本地时间的2000-01-01
new Date(Date.UTC(2000,0)) // Sat Jan 01 2000 08:00:00 GMT+0800 (中国标准时间)
4.1.3 Date.now()
表示执行时日期和时间的毫秒数。
Date.now() // 1619774407628
4.1.4继承的方法
toLocaleString()
: 返回与浏览器运行的本地环境一致的日期和时间。toString()
: 返回带时区信息的日期和时间。valueOf()
: 返回的是日期的毫秒表示。因此比较日期大小时,可以直接使用它的返回值。
let date = new Date()
date.toLocaleString() // 2021/4/30 下午5:28:20
date.toString() // Fri Apr 30 2021 17:28:47 GMT+0800 (中国标准时间)
date.valueOf() // 1619774874188
其他方法,详见
4.2 RegExp
正则表达式有两种书写方式,第一种是let reg = /pattern/flags
,第二种是let reg = new RegExp(pattern, flags)
, 例如下面两个是等效的。
let reg1 = /[bc]at/i
let reg2 = new RegExp("[bc]at", "i") // 这两个参数必须都是字符串
其中标记(flags
)可以有以下值:
g
: 全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束了。i
: 匹配时会忽略大小写。m
: 多行模式,表示查找到一行文本末尾时会继续查找。y
: 粘附模式,表示只查找从lastIndex
开始以及之后的字符串。u
:Unicode
模式,启用Unicode
模式匹配。s
:dotAll
模式,表示元字符.
匹配任何字符(包括\n
和\r
)。
所有元字符在模式中必须转义,包括:(
[
{
\
^
$
|
}
]
)
?
*
+
.
因为RegExp
的模式参数是字符串,所以在某些情况下需要二次转义。所有元字符都必须二次转义,包括转义字符序列,如\n
(\
转义后是\\
,在正则表达式中则要写成\\\\
), 下面展示了正则表达式字面量形式以及使用RegExp
构造函数创建时对应的模式字符串。
字面量模式 | 对应的字符串 |
---|---|
/\[bc\]at/ | \\[bc\\]at |
/\.at/ | \\.at |
/name\/age/ | name\\/age |
/\d.\d{1,2}/ | \\d.\\d{1,2} |
/\w\\hello\\123/ | \\w\\\\hello\\\\123 |
4.2.1 RegExp
实例属性
每个实例都有以下属性
global
: 布尔值,表示是否设置了g
标记ignoreCase
: 布尔值,表示是否设置了i
标记unicode
: 布尔值,表示是否设置了u
标记sticky
: 布尔值,表示是否设置了y
标记lastIndex
: 整数,表示在源字符中下一次搜索的开始位置,从0
开始multiline
: 布尔值,表示是否设置了m
标记dotAll
: 布尔值,表示是否设置了s
标记source
: 正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠flags
: 正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回。(没有前后斜杠)
let reg1 = /\[bc\]at/i
console.log(reg1.source) // "\[bc\]at"
console.log(reg1.flags) // "i"
let reg2 = new RegExp("\\[bc\\]at", "i")
console.log(reg2.source) // "\[bc\]at
console.log(reg2.flags) // "i"
4.2.2 RegExp
实例方法
exac()
:用于配合捕获组匹配,参数是一个字符串。如果找到匹配项,则返回第一个包含匹配信息的数组;如果没有找到匹配项,则返回null
。返回的数组虽然是Array
的实例,但是包含两个额外的属性:index
和input
。index
是字符中匹配模式的起始位置,input
是要找的字符串。这个数组的第一个元素是匹配整个模式的字符串,其他元素是与表达式中的捕获组匹配的字符串。
let text = "mom and dad and baby"
let pattern = /mon( and add( and baby)?)?/gi
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches.input) // "mom and dad and baby"
console.log(matches[0]) // "mom and dad and baby"
console.log(matches[1]) // " and dad and baby"
console.log(matches[2]) // " and baby"
如果模式设置了全局标记,则每次调用exec()
方法会返回一个匹配的信息。如果没有设置全局标记,则无论对同一个字符串调用了多少次exec()
, 也只会返回第一个匹配信息。
let text = "cat, bat, sat, fat"
let pattern = /.at/;
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
let text = "cat, bat, sat, fat"
let pattern = /.at/g;
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 3
matches = pattern.exec(text)
console.log(matches.index) // 5
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 8
如果设置了粘附标记y,则每次调用exec()
就只会在lastIndex
的位置上寻找匹配项。粘附标记覆盖全局标记。
let text = "cat, bat, sat, fat"
let pattern = /.at/y
let matches = pattern.exec(text)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 3
matches = pattern.exec(text)
console.log(matches) // null
console.log(pattern.lastIndex) // 0
pattern.lastIndex = 5;
matches = pattern.exec(text)
console.log(matches.index) // 5
console.log(matches[0]) // bat
console.log(pattern.lastIndex) // 8
test()
方法接收一个字符串,如果输入的文本与模式匹配,则返回true
,否则返回false
。
let text = "000-00-0000"
let pattern = /\d{3}-\d{2}-\d{4}/
if(pattern.test(text)) {
console.log('matched')
}
4.2.3 RegExp
构造函数属性
RegExp
构造函数本身也有几个静态属性。这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。每个属性都有一个全名和简写。
全名 | 简写 | 说明 |
---|---|---|
input | $_ | 最后搜索的字符串(非标准特性) |
lastMatch | $& | 最后匹配的文本 |
lastParen | $+ | 最后匹配的捕获组(非标准特性) |
leftContext | $` | input 字符串中出现lastMatch 前面的文本 |
input | $' | input 字符串中出现lastMatch 后面的文本 |
这些属性可以提取exec()
和test()
执行操作相关的信息。
let text = "this has been a short summer"
let pattern = /(.)hort/g
if(pattern.test(text)) {
console.log(RegExp.input) //this has been a short summer
console.log(RegExp.leftContext) // this has been a
console.log(RegExp.rightContext) // summer
console.log(RegExp.lastMatch) // short
console.log(RegExp.lastParen) // s
}
RegExp
还有其他几个构造函数属性,可以存储最多9个捕获组的匹配项。这些属性通过RegExp.$1~RegExp$9
来访问,分别包含1~9个捕获组的匹配项。在调用exec()
和test()
时,这些属性就会被填充。
let text = "this has been a short summer"
let pattern = /(..)ho(.)/g
if(pattern.test(text)) {
console.log(RegExp.$1) // sh
console.log(RegExp.$2) // t
}
4.3 原始值包装类型
为了方便操作原始值,ECMAScript
提供了3种特殊的引用类型: Boolean
, Number
, String
。每当调用到某个原始值的方法或属性时,后台都会创建一个相应原始值包装类型的对象,从而暴露出操作原始值的各种方法。
let s1 = "text"
let s2 = s1.substring(2)
我们知道原始值不是对象,本不应该存在任何方法。但是上面的语句却执行成功了。具体来说,当第二行访问s1时,是以读模式访问的,也就是从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下3步:
- 创建一个
String
类型的实例 - 调用实例的方法
- 销毁实例
可以把以上3步想象成了如下代码:
let s1 = new String('text')
let s2 = s1.substring(2)
s1 = null
引用类型和原始值包装类型的主要区别在于对象的生命周期,在通过new
实例化后的引用类型后,得到的实例会在离开作用域时被销毁。而自动创建的原始值包装对象则只存在于访问他的那行代码执行期间。这意味着不能再运行时给原始值添加属性和方法。
let n = 1
n.color = 'red'
console.log(n.color) // undefined
上述第三行代码为undefined
的原因是,第二行代码运行时,会创建一个Number
对象,而当第三行代码执行的时候,这个对象已经被销毁了,所以为undefined
。
我们虽然可以显示的调用Number
, Boolean
, String
构造函数来创建原始值包装对象,不过没有必要这么做,因为在原始值包装对象的实例调用typeof
会返回object
。
另外,Object
构造函数能够通过传入值的类型返回相应原始值包装类型的实例:
let obj = new Object("text")
console.log(obj instanceof String) // true
4.3.1 Number
toFixed()
: 接收一个参数,表示返回的数值字符串要包含多少位的小数
let n = 1
console.log(n.toFixed(2)) // "1.00"
toExponential()
: 返回以科学计数法表示的数值字符串。接收一个参数,表示结果中小数的位数。
let n = 1
console.log(n.toExponential(n)) // "1.0e+0"
toPrecision()
:会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学技术法形式。接收一个参数,表示结果中数字的总位数(不包括指数)。
let n = 99
console.log(n.toPrecision(1)) // 1e+2 首先用一位数字表示数值99,得到1e+2也就是100,因为99不能只用一位数值来精确表示。所以就将它舍入为100,这样就可以用科学计数法表示。
console.log(n.toPrecision(2)) // 99
console.log(n.toPrecision(3)) // 99
isInteger()
方法与安全系数
Number.isInteger()
方法用于辨别一个数值是否保存为整数。有时候小数位的0可能会让人误以为数值是一个浮点值:
console.log(Number.isInteger(1)) // true
console.log(Number.isInteger(1.00)) // true
console.log(Number.isInteger(1.01)) // false
IEEE 754
数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个范围从Number.MIN_SAFE_INTEGER()
(-253 + 1)到Number.MAX_SAFE_INTEGER
(253 + 1)。超出这个范围的数值,即使尝试保存为整数,IEEE 754
编码格式也意味着二进制值可能会表示一个完全不同的数值。为了鉴别整数是否在这个范围,可以使用Number.isSafeInteger()
方法:
console.log(Number.isSafeInteger(-1 * (2 ** 53))) // false
console.log(Number.isSafeInteger(-1 * (2 ** 53) + 1)) // true
console.log(Number.isSafeInteger(2 ** 53)) // false
console.log(Number.isSafeInteger((2 ** 53) - 1)) // true
4.3.2 String
js字符串由16位码元(code unit
)组成。对大多数字符串来说,每16位码元对应一个字符。换句话说,字符串的length
属性表示字符串含有多少个16位码元。
对于U+0000~U+FFFF
范围的字符串,length
,charAt()
,charCodeAt()
,fromCharCode()
返回的结果都和预期一样。
let str = 'abcd'
console.log(str.length) // 4
console.log(str.charAt(2)) // c
console.log(str.charCodeAt(2)) // 99 (注:c对应的编码是U+63,变为十进制就是99)
console.log(String.fromCharCode(97,98,99,100)) // "abcd"
但是,这个对应关系在扩展到Unicode
增补字符平面时就不成立了。问题很简单,即16位只能表示65536个字符。这对于大多数语言字符集是够了,在Unicode
中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode
采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符串使用两个16位码元的策略称为代理对。在涉及增补平面的字符时,前面说的属性和方法就会出现问题。
let str = "ab😊cd" // 😊对应的编码为U+1f60a,十进制128522
console.log(str.lenght) // 6
console.log(str.charAt(1)) // b
console.log(str.charAt(2)) // �
console.log(str.charAt(3)) // �
console.log(str.charCodeAt(1)) // 98
console.log(str.charCodeAt(2)) // 55357
console.log(str.charCodeAt(3)) // 56842
console.log(String.fromCharCode(97, 98, 55357, 56842, 99, 100)) // ab😊cd
这些方法仍将16位码元当做一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。fromCharCode()
方法仍然返回正确的结果,因为他实际上是基于提供的二进制表示直接组合成字符串,浏览器可以正确解析代理对。
为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用codePointAt()
方法。它可以从指定码元的位置上识别完整的码点。
let str = "ab😊cd"
console.log(str.codePointAt(1)) // 98
console.log(str.codePointAt(2)) // 128522
console.log(str.codePointAt(3)) // 56842
注:如果传入的码元索引并非代理对的开头,就会返回错误的码点
迭代字符串可以智能的识别代理对的起点
console.log([..."ab😊cd"]) // ["a", "b", "😊", "c", "d"]
fromCharCode()
也有一个对应的fromCodePoint()
。该方法接收任意数量的码点,返回对应字符拼接起来的字符串。
console.log(String.fromCharCode(97,98,55357,56842,99,100)) // ab😊cd
console.log(String.fromCodePoint(97,98,128522,99,100)) // ab😊cd
normallize()
某些Unicode
字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示:
let a1 = String.fromCharCode(0x00c5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A)
console.log(a1, a2, a3) // Å Å Å
console.log(a1 === a2) // false
console.log(a2 === a3) // false
console.log(a3 === a1) // false
为解决这个问题,Unicode
提供了4中规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。NFD
(Nomalization Form D),NFC
(Normalization Form C),NFKD
(Normalization Form KD), NFKC
(Normalization Form KC)。可以使用normalize()
方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:NFD
,NFC
,NFKD
,NFKC
。
let a1 = String.fromCharCode(0x00c5)
// U+00c5是对0+212B进行NFC/NFKC规范化后的结果
console.log(a1 === a1.normalize("NFD")) // false
console.log(a1 === a1.normalize("NFC")) // true
console.log(a1 === a1.normalize("NFKD")) // false
console.log(a1 === a1.normalize("NFKC")) // true
concat()
它可以接收任意多个参数,因此可以一次性拼接多个字符串:
let str = 'hello '
let res = str.concat('world', '!') // hello world!
虽然这个方法可以拼接多个字符串,使用加号更方便。
slice()
,substr()
,substring()
这几个方法都接受一或两个参数,分别表示开始或结束的位置,并且不会修改源字符串,只会返回截取到的字符串。
slice() | substring() | substr() |
---|---|---|
参数2是提取到结束位置下标(不包括结束位置) | 同slice() | 参数2表示返回的字符串数量 |
省略参数2都意味着提取到字符串末尾 | 同slice() | 同slice() |
将所有负值参数都当做字符串长度加上负值 | 将所有负值参数都转为0 | 参数1为负,则为字符串长度加该值,参数2为负则转为0 |
省略第二个参数意味着截取到字符串末尾 | 同slice() | 同slice() |
如果参数1大于参数2,则返回空字符串 | 如果参数1大于参数2,则会将参数1和参数2进行调换 | 参数1和参数2的大小没有关系,因为此时的参数2表示截取的长度 |
indexOf()
和lastIndexOf()
这两个方法从字符串中搜索传入的字符串,并返回位置(如果没有找到,则返回-1)。两者区别在于,indexOf()
方法从字符串开头查找,lastIndexOf()
则从字符串末尾开始查找。
这两个方法也可以接受第二个可选参数,表示搜索的开始的位置。indexOf()
会从该位置向后搜索,lastIndexOf()
则相反。
startsWith()
,endsWith()
,includes()
,split()
这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。
startsWith()
和includes()
方法接收可选的第二个参数,表示开始搜索的位置。endsWith()
方法也可以接受可选的参数,表示应该当做字符串末尾的位置。
let str = 'foobarbaz'
console.log(str.startsWith("foo")) // true
console.log(str.startsWith("bar")) // fasle
console.log(str.startsWith("bar",3)) // true
console.log(str.endsWith("baz")) // true
console.log(str.endsWith("bar")) // false
console.log(str.endsWith("bar", 6)) // true
console.log(str.includes("bar")) // true
console.log(str.includes("bus")) // false
console.log(str.includes("bar", 3)) // true
trim()
这个方法会创建一个字符串的副本,删除前,后所有空格符,在返回结果。
repeat()
方法
该方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。
let str = 'na '
console.log(str.repeat(2) + 'batman') // na na batman
padStart()
,padEnd()
这两个方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数都是长度,第二个参数是可选的填充字符串,默认为空格。
let str = 'foo'
console.log(str.padStart(6)) // " foo"
console.log(str.padStart(6, '.')) // "...foo"
console.log(str.padEnd(6)) // "foo "
console.log(str.padEnd(6, '.')) // "foo..."
可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。
let str = "foo"
console.log(str.padStart(8, "bar")) // "barbafoo"
console.log(str.padStart(2)) // "foo"
console.log(str.padEnd(8, "bar")) // "forbarba"
console.log(str.padEnd(2)) // "foo"
- 迭代与解构
字符串可以使用for-of
迭代,也可以通过解构操作符解构。
console.log([..."abc"]) // ["a", "b", "c"]
toLowerCase()
,toLocaleLowerCase()
,toUpperCase()
,toLocaleUpperCase()
前两个方法是变为小写,后两个方法是变为大写。如果不知道代码涉及什么语言,则最好使用地区特定的转换方法。
match()
,search()
,replace
match()
方法接收一个参数,可以是一个正则表达式,也可以是字符串, 它返回的数组与RegExp
对象的exec()
方法返回的数组是一样的:第一个元素是与整个模式匹配的字符串,其余元素则是捕获组匹配的字符串(如果有的话)。
let str = "cat, bat, sat, fat"
let pattern = /.at/
// 等价于 pattern.exec(str)
let matches = str.match(pattern)
console.log(matches.index) // 0
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
如果是全局匹配的话,则只会返回与整个模式匹配的字符串数组,不包括捕获组
let str = "cat, bat, sat, fat"
let pattern = /(.)at/g
let matches = str.match(pattern)
console.log(matches.index) // undefined
console.log(matches[0]) // cat
console.log(pattern.lastIndex) // 0
console.log(matches) // ["cat", "bat", "sat", "fat"]
search()
接收一个参数,可以是字符串也可以是正则表达式。该方法返回模式第一个匹配的位置索引,如果没有找到,则返回-1,它是从头向后开始匹配。
let str = "cat, bat, sat, fat"
let pattern1 = /bat/
let pattern2 = /bat/g
console.log(str.search(pattern1), str.search(pattern2)) // 5 5
replace()
接收两个参数,第一个参数可以是正则表达式或字符串,第二个参数可以是一个字符串或一个函数。如果第一个参数是一个字符串,则只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并带有全局标记。
let str = "cat, bat, sat, fat"
console.log(str.replace('at', 'nod')) // cnod, bat, sat, fat
console.log(str.replace(/at/g, 'nod')) //cnod, bnod, snod, fnod
第二个参数是字符串的情况下,有几个特殊的字符串序列,可以用来插入正则表达式操作的值。
字符序列 | 替换文本 |
---|---|
$$ | $ |
$& | 匹配整个模式的子字符串,与RegExp.lastMatch 相同 |
$' | 匹配的子字符串之后的字符串,与RegExp.rightContext 相同 |
$` | 匹配的子字符串之前的字符串,与RegExp.leftContext 相同 |
$n | 匹配第n个捕获组的字符串,其中n是0-9,如果没有捕获组则为空字符串 |
$nn | 匹配第nn个捕获组字符串,其中nn是01-99,如果没有捕获组,则值为空空字符串 |
第二个参数可以是一个函数。在只有一个匹配项时,这个函数会受到3个参数:与整个模式匹配的字符串,匹配项在字符串中的开始位置,以及整个字符串。在有多个捕获组的情况下,每个匹配捕获组的的字符串也会作为参数传给这个函数,但最后两个参数还是与整个模式匹配的开始位置和原始字符串。如果是全局匹配,则每匹配一次,就会执行一次这个函数,函数的参数和只有一个匹配项一样。
split()
会根据传入的分隔符将字符串拆分为数组。参数可以是字符串,也可以是正则表达式。还可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。
let str = "blue,white,yellow"
console.log(str.split(',')) // ["blue", "white", "yellow"]
console.log(str.split(',', 2)) // ["blue", "white"]
console.log(str.split(/[^,]+/)) // ["", ",", ",", ""]
localeCompare()
比较两个字符串,返回如下3个值中的一个。
如果按照字母表顺序,字符串应该排在字符串参数前面,则返回负值(通常是-1,具体还要看与实际值相关的实现); 如果字符串与字符串参数相等,则返回0; 如果按照字母表顺序,字符串应该排在字符串参数后面,则返回正值(通常是1,具体还要看与实际值相关的实现);
let str = 'yellow'
console.log(str.localeCompare("brick")) // 1
console.log(str.localeCompare("yellow")) // 0
console.log(str.localeCompare("zoo")) // -1
这个方法独特之处在于,实现所在的地区决定了这个方法如何比较字符串。
4.4 单例内置对象
ECMA-262
对内置对象的定义是:任何由ECMAScript
实现提供、与宿主环境无关,并在ECMAScript
程序开始执行时就存在的对象。
4.4.1 Global
Global
对象时最特别的对象,因为代码不会显示的访问它。在全局作用域中定于的变量和函数都会变成它的属性。虽然ECMA-262
没有规定直接访问它的方式,但浏览器将window
对象实现为Global
对象的代理。
URL
编码方法
encodeURI()
和encodeURIComponent()
方法用于编码统一资源标识符(URI)。encodeURI()
方法用于对整个URI进行编码。encodeURIComponent()
用于编码URI中单独的组件。主要区别是:encodeURI()
不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent()
会编码它发现的所有非标准字符。
let uri = 'http://www.baidu.com/test value.js#start'
// http://www.baidu.com/test%20value.js#start
console.log(encodeURI(uri))
// http%3A%2F%2Fwww.baidu.com%2Ftest%20value.js%23start
console.log(encodeURIComponent(uri))
使用encodeURI()
编码整个URI,使用encodeURIComponent()
编码已有URI后面的字符串。一般来说,使用encodeURIComponent()
比使用encodeURI()
的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。
与encodeURI()
和encodeURIComponent()
相对的是decodeURI()
和decodeURIComponent()
注:上面四种方法取代了escape()
和unescape()
方法,后者在ECMA-262第三版中就已经废弃了。URI方法始终是首选方法,因为它对所有的Unicode
字符进行编码,而后者只能正确编码ASCII
字符。
eval()
它是一个完整的ECMASCript
解释器,它接收一个参数,即一个要执行的js字符串。
eval("console.log('hello')")
上面的代码等同于下一行代码
consolelog('hello')
通过eval()
执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。
let msg = 'hello'
eval("console.log(msg)") // "hello"
通过eval()
定义的任何变量和函数都不会被提升。这是因为在解析代码时,它们是被包含在字符串中的,它们只在eval()
函数执行的时候才会被创建。
注: 在严格模式下,给eval
赋值会报错。在eval()
内创建的函数或变量不会被外部访问,但是eval()
函数内部可以访问外部变量。解释代码字符串的能力非常强大,但也是非常危险的。在使用它时必须慎重。特别是在解释用户输入的内容时。因为这个方法很容易被XSS攻击。
4.4.2 Math
它提供了一些辅助计算的属性和方法。
注:Math
对象上提供的计算要比直接在js实现的快得多,因为Math
上的计算使用js引擎中更搞笑的实现和处理器指令。但使用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的值 |
Math.SQRT2 | 2的平方根 |
min()
,max()
let values = [1,10,20,5]
console.log(Math.min(...values)) // 1
console.log(Math.max(...values)) // 20
- 舍入方法
Math.ceil()
:方法始终向上舍入为最接近的整数。
Math.floor()
:方法始终向下舍入为最接近的整数。
Math.round()
:四舍五入
Math.fround()
:返回数据最接近的单精度(32位)浮点值表示。
random()
返回0-1范围内的随机数,其中包含0不包含1。
注: 如果是为了加密而需要生成随机数,那么建议使用window.crypto.getRandomValues()
- 其他方法