【You Don't Know JS: Types & Grammar】Chapter 2 笔记

阵列(Array)

可以将阵列视为能够装载任何值的容器,不论是number , string , object,甚至是另一个阵列(产生多维阵列)。
http://img2.58codes.com/2024/20112573fYMvtrBedY.jpg

无需为阵列先预设大小,它会自动维护length的值。
http://img2.58codes.com/2024/20112573Qi0ZSsIxwa.jpg

请避免建立「稀疏(sparse)」阵列,使阵列产生空插槽(empty slots)。
http://img2.58codes.com/2024/20112573ZVeITLDIgO.jpg

稀疏阵列不会发生错误,但请避免这样的方式。

稀疏阵列的a[1]显示undefined,但这跟我们明确指定a[1] = undefined还是有所不同。

阵列最大的特点就是以数值来索引的,但阵列也是物件,所以它也拥有物件的行为「string键值(keys)/属性(properties)」。

如若使用string键值,length将不列入计算。
http://img2.58codes.com/2024/20112573rvHh5HnRCF.jpg

如果,键值能够被强制转型为十进位的number,那JS就会将之当作number索引使用,而非string键值。
http://img2.58codes.com/2024/20112573yfSV3b2yqa.jpg

虽然阵列可以使用string键值(keys)/属性(properties),然而,阵列最大的特点就是使用number作为索引。

与其如此,不如直接使用object就好。

类阵列(Array-Links)

以数值做索引的集合,但非真正的阵列。

例如,DOM的查询,都会回传由DOM元素所组成的串列(list),但非阵列。

如果我们要进行转换,可使用ES6的新功能Array.form(),会在ES6笔记另行介绍。

字串(string)

注意,这观点修正了笔者之前对字串(string)的认知

一般的认知是「string是字元(characters)所组成的阵列」,表面看似如此,但实际上有些行为却是大不相同。
http://img2.58codes.com/2024/201125732LrXR4OZd2.jpg

以上的结果都一样,似乎string就是字元(characters)所组成的阵列。

但,不尽然如此。

http://img2.58codes.com/2024/20112573vKkGSzWXQi.jpg

字串的"o"依然是小写,但阵列却改变了,这正是字串与阵列最大的差异点。

JS中的字串是不可变(immutable)的,而阵列是可变(mutable)的。

另外,a[1]这种方式,并非洽当,较正确的作法是a.charAt(1)

不可变的意思是,字串的内容无法就地(in-place)修正,而是会回传一个新的字串。

可变的意思是,阵列的内容可以就地修正。

http://img2.58codes.com/2024/20112573KCAekAoytw.jpg

我们也可以使用不会更动内容的阵列方法join( )以及map( )

因为目标为字串,所以我们藉由Function.prototype.call来呼叫阵列方法。

http://img2.58codes.com/2024/20112573SN4K0fY8pT.jpg

反转字串(reversing string)

字串并没有像阵列一样拥有反转方法reverse( )

因为字串是不可变的,所以也无法使用Array.prototype.reverse.call(a),会掷出错误。

变通就是将字串转为阵列,再反转,再转为字串。
http://img2.58codes.com/2024/20112573tUyo5LjMXW.jpg

如果我们对于字串的处理较常当作阵列的对待的话,比较好的方式是直接把字串储存为阵列,而非字串,省去转来转的去的麻烦,若需要使用字串表示,再使用join()方法转成字串。

数值(number)
JS中只有一种数值型别「number」。包含整数以及带有小数点的十进位数字。

JS并没有真正的整数,89跟89.0一样都是整数。

JS number的实作是基于「IEEE754」标準的「双精度(double precision)」格式(64位元的二进位数字)。

数值语法

数字字面值通常以10为基底(base-10)表示。
http://img2.58codes.com/2024/201125734IAC4dQlzf.jpg

小数点之后的0会被移除。
http://img2.58codes.com/2024/20112573dHRs4LQAWS.jpg

非常大或非常小的数值预设会以指数形式输出,与toExponential()方法输出的格式相同。
http://img2.58codes.com/2024/20112573IOmdwn0YXK.jpg

toFixed()方法能够指定使用几个小数位数。
http://img2.58codes.com/2024/201125737XL7xOxFcz.jpg
注意,输出的型别为字串,若要求超过数值本身的位数,会以0补齐。

toPrecision()方法指定的是多少位的有效数字(significant digits)
http://img2.58codes.com/2024/20112573OrfYIy7G7i.jpg

也可以不透过变数,直接在数字字面值上处理,不过这种方式相当少见。

也可以使用指数形式来表示很大的字面值。
http://img2.58codes.com/2024/20112573WeEUdxmBa3.jpg

小数值的陷阱
http://img2.58codes.com/2024/201125738jecJ9137j.jpg

从数学上来说0.1+0.2=0.3是绝对成立的,但为何答案是false?

原因就在于IEEE754的二进位浮点数表示并不是完全100%的精确,还是有极小的误差。
http://img2.58codes.com/2024/20112573ccylc8yo1J.jpg

所以,当处理小数值的时候,要特别注意这种情况,但大部分处理整数的时候,JS的数值表示是安全的。

如果真的要比较0.1+0.2与0.3的话,我们可以使用「约整误差(rounding error)」值作为比较误差的容许值(2^-52 (2.220446049250313e-16) )。

ES6的Number.EPSILON已经预先定义该容许值

所以我们可以这样使用:
http://img2.58codes.com/2024/20112573CBI1b7fU7N.jpg

Number.MAX_VALUE,能够表示的最大浮点数。
Number.MIN_VALUE,能够表示的最小浮点数。
http://img2.58codes.com/2024/20112573F8bSflD1Mx.jpg

安全的整数範围

JS中number值的表示,有一个安全範围(所请求的值都可以精确地表示)

ES6中已预先定义安全值。

Number.MAX_SAFE_INTEGER,能够表示的最大安全值。
Number.MIN_SAFE_INTEGER,能够表示的最小安全值。
http://img2.58codes.com/2024/20112573gYL85EDm6e.jpg

JS遇到大数字的情况大多是处理资料库的64位元ID,此类的值无法以number精确地表示出来,所以在JS中都以string储存或传输。

测试整数

若要测试某个值是否为整数,可使用ES6规格的Number.isInteger( )
http://img2.58codes.com/2024/201125736tA3mq8zNL.jpg

若要测试某个值是否为安全整数(safe integer),可使用ES6规格的Number.isSafeInteger( )
http://img2.58codes.com/2024/201125738boPqplV6o.jpg

特殊值

undefined型别只有「undefined」值。

null型别只有「null」值。

undefined与null的差异:

null是一个空值。undefined是一个缺少(missing)的值。

或是

undefined代表尚未有值。null代表曾有过值,但现在没有了。

null是一个关键字(keyword),而非识别字(identifier),意味着它不能是变数。

undefined是识别字,在非strict模式中,可以指定值给undefined识别字,但非常不建议这样做。

NaN

执行数学运算,若无法产生一个有效值,此时就会得到NaN值。

NaN按照字面上解释,代表「not a number」(不是一个数字)。但更为洽当的解释应该将之视为「无效的数字」、「不合格的数字」或是「坏掉的数字」更为适合。

http://img2.58codes.com/2024/20112573rCEun5prIg.jpg

既然已经明确指出a为NaN,但它的型别却是number,这是令人比较困惑的地方。

发生NaN可以解释的情况是「我们尝试做数学运算,但失败了,所以得到一个失败的number结果」。

如果我们想要确认变数是否为NaN值的话,使用下列方式会导致失败。
http://img2.58codes.com/2024/20112573AZIOogQeLn.jpg

NaN有个特别之处就是,它永远都不等于自己。

可以使用isNaN( )来测试。
http://img2.58codes.com/2024/20112573biuQZMYTos.jpg

问题看似解决了,却还是有缺陷。
http://img2.58codes.com/2024/20112573NwmwukI6kU.jpg
"foo"的确不是number,但它也不是NaN。会造成这结果主要是因为isNaN( )太过依赖于NaN这个字面上的意义了。

ES6提供了一个解决方案,Number.isNaN( )方法,我们还可以利用它永远都不等于自己的特性来判断。
http://img2.58codes.com/2024/20112573Fp8aWRJ7ZD.jpg

虽然看似奇怪,但的确可行。

无限值

在传统的程式语言执行除以0的数学运算,会发生除以0的错误。

但在JS中,并不会发生错误,而是会产Infinity/-Infinity值。
http://img2.58codes.com/2024/20112573ScsFxtS7vg.jpg

ES6以经预先定义了无限值。
http://img2.58codes.com/2024/20112573T989NZKlYc.jpg

JS使用的是有限的数值表示法,所以像是数学运算的结果,有可能会产生溢位(overflow)。
http://img2.58codes.com/2024/20112573yQuYAZdZzG.jpg

如果运算结果产生了超出显示範围的值,就会依据IEEE754的「约整至最接近值(round-to-nearest)」的规範来决定显示结果。

a + Math.pow( 2, 969 )的结果比较接近Number.MAX_VALUE,而非Infinity,所以会「向下约整(rounds down)」,显示Number.MAX_VALUE。

a + Math.pow( 2, 969 )的结果比较接近Infinity,所以会「向上约整(rounds up)」,显示Infinity。

从数学角度来看,无限除以无限是一个无法定义的运算,所以,Infinity/Infinity的结果会是NaN。

若把任何有限的number除以Infinity,那结果会是0。
http://img2.58codes.com/2024/201125733gGVbxMrpA.jpg

特殊相等性

ES6新增一个测试特殊值相等性的方法,Object.is( )方法。

它可以测试NaN与-0的值是否相等。
http://img2.58codes.com/2024/20112573q9DB5WDVec.jpg

值 & 参考

值可藉由值的拷贝(value-copy)或参考的拷贝(reference-copy)来指定或传递。

在JS中,没有指标(pointers)这种机制,无法从某个变数指向另一个变数的参考。

在JS中,值的型别完全决定该值是藉由值的拷贝(by value-copy)或参考的拷贝(by reference-copy)来指定的。

http://img2.58codes.com/2024/20112573P2xFXsJs8R.jpg

纯量的基型值(primitives)永远都是藉由值的拷贝(by value-copy)来指定或传递的:null、undefined、string、number、boolean。

複合值的object(array、封装的物件包裹器boxed object wrappers、与function),永远都是藉由参考的拷贝(by reference-copy)来指定或传递的。

以上面的例子来说,a的值是2,为纯量的基型值(primitives),b就会被指定该值的另一份拷贝,变更b时,不会动到a的值。

c和d是指向[1,2,3]的个别参考,两者所参考的都是同一个[1,2,3],当透过其中一个参考来更动array时,影响到的是唯一共有值[1,2,3],而两者之后都会参考到经过修改的新值[1,2,3,4]。

http://img2.58codes.com/2024/20112573lHC2EqGDBI.jpg
一开始a跟b都指向同一个值[1,2,3],之后我们将b的参考指向另一个值[4,5,6],这并不会影响到a的参考。
这时,a跟b个别指向不同的值。

function foo(x) {x.push( 4 );x; //[1,2,3,4]x = [4,5,6];x.push( 7 );x; //[4,5,6,7]}var a = [1,2,3];foo( a );a; //[1,2,3,4]

当我们将a传给foo( )时,它会将a的参考拷贝给x,所以ax是指向同一个值([1,2,3])的不同参考。接下来,我们使用push( 4 )来变更值。

但是,执行x = [4,5,6]的时候,我们只是把x的参考指向[4,5,6]a的参考并无变动。

所以,我们无法透过x的参考来改变a要指向哪个值。我们只能修改ax所指向共有值的内容。

如若要让a持有[4,5,6,7],就必须修改a所指向的值的内容。

function foo(x) {x.push( 4 );x; //[1,2,3,4]x.length = 0;x.push( 4, 5, 6, 7 );x; //[4,5,6,7]}var a = [1,2,3];foo( a );a; //[4,5,6,7]

执行x.length = 0x.push( 4, 5, 6, 7 )会直接修改共有的array,所以a自然就会持有[4,5,6,7]

注意,我们无法控制值拷贝(by value-copy)或参考拷贝(by reference-copy)的行为,一切都由值的型别来决定。

如果我们希望,array的值,能够有值拷贝(by value-copy)行为的话,就必须透过slice( )方法,它能複製出所指向值的浅层拷贝,以上面的例子来说,foo( a.slice() ),所传入的是藉由slice( )製作出来的a参考的拷贝,并非a的参考,因此不会影响a所参考的值。

如果,我们希望基型值(primitives)能有参考拷贝(by reference-copy)的行为该怎么做?

那就得将值包在能有有参考拷贝(by reference-copy)行为的型别(object、array)之中了。

function foo(wrapper) {wrapper.a = 42;}var obj = {a: 2};foo( obj );obj.a; // 42

物件obj的属性a值是基型值(primitives),将obj传入foo( ),行为就像是参考拷贝(by reference-copy),透过wrapper.a的指定,也会影响obj.a。

JS并没有指标,不会指向其他变数或参考,只会指向底层的值。

参考来源:
http://img2.58codes.com/2024/201125739FY75WdXxA.jpg

此为You Don't Know JS系列的笔记。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章