精读《你不知道的JavaScript》中卷-I-第2章 值

152 阅读7分钟

I-第2章 值

通过这章的学习,可以了解,数组,字符串,特殊数值,值传递和值引用的区别

数组

JS的数组可容纳任何类型的值,数组声明后即可添加值,不用预设大小。

let a = [1, '2', [3]];
a.length; // 3
a[2][0] === 3; // true

a[4] =  5;
a[3]; // undefined

a[3]为undefined,称作"稀疏数组"(sparse array)。这里的undefined与显示赋值为undefined不同。

数组也是对象。可以自定义属性(不计算在数组长度内)。

let a = [1];
a['foo'] = 2;
a.length; // 1
a['foo']; // 2

类数组

类数组转数组常用方法

// slice() 返回参数列表的一个数组复本。
var arr = Array.prototype.slice.call( arguments );

var arr = Array.from( arguments );

字符串

字符串和数组相似,都有length属性,indexOf, concat方法

let a = 'foo';
let b = ['f', 'o', 'o'];

a.length; // 3
b.length; // 3
a.indexOf('o'); // 1
b.indexOf('o'); // 1

var c = a.concat( 'bar' );// 'foobar'
var d = b.concat( ['b','a','r'] );// ['f','o','o','b','a','r']
a === c;// false

b === d;// false

a; // 'foo'

b; // ['f','o','o']

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

借助数组方法来处理字符串

a.join;   // undefined
a.map;   // undefined

var c = Array.prototype.join.call( a, '-' );
var d = Array.prototype.map.call( a, function(v){
    return v.toUpperCase() + '.';
} ).join( '' );

c;    // 'f-o-o'
d;    // 'F.O.O.'

字符串反转

字符串没有reverse方法

var c = a
// 将a的值转换为字符数组
.split( "" )
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join( "" );
c; // "oof"

如果经常以字符数组方式处理字符串的话,倒不如直接用数组。在需要时使用join()将数组转成字符串。

数字

toFixed()返回的是字符串 toPrecision(..) 方法用来指定有效数位的显示位数

var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"

var a = 42.59;

a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"

不过对于 . 运算符需要给予特别注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。

42.tofixed(3) 是无效语法,因为 . 被视为常量 42. 的一部分(如前所述),所以没有 . 属 性访问运算符来调用 tofixed 方法。 42..tofixed(3) 则没有问题,因为第一个 . 被视为 number 的一部分,第二个 . 是属性访问运算符。

// invalid syntax:
42.toFixed( 3 ); // SyntaxError

// these are all valid:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000"

怎样判断0.1+0.2 === 0.3

最常见的方法是设置一个误差范围值,通常称为“机器精度”(machine epsilon),对JavaScript的数字来说,这个值通常是 2^-52 (2.220446049250313e-16)。 从 ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用,也可以为 ES6 之前 的版本写 polyfill:

if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2,-52);
}

可以使用 Number.EPSILON 来比较两个数字是否相等(在指定的误差范围内):

function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b );  // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

数字“安全”呈现的最大整数是2^53 - 1,即9007199254740991,在ES6中被定义为 Number.MAX_SAFE_INTEGER。 最小整数是 -9007199254740991, 在 ES6 中 被 定 义 为 Number. MIN_SAFE_INTEGER。

整数检测Number.isInteger(..)

Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false

// polyfill
if (!Number.isInteger) {
    Number.isInteger = function(num) {
        return typeof num == "number" && num % 1 == 0;
    };
}

检测安全的整数Number.isSafeInteger(..)

Number.isSafeInteger( Number.MAX_SAFE_INTEGER );// true
Number.isSafeInteger( Math.pow( 2, 53 ) );// true
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 );// false

// polyfill
if (!Number.isSafeInteger) { Number.isSafeInteger = function(num) {
    return Number.isInteger( num ) &&
        Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
    };
}

特殊数值

undefined和null

undefined 类型只有一个值,即 undefined。null 类型也只有一个值,即 null。它们的名 称既是类型也是值。

  • null 指空值(empty value),曾赋过值,但是目前没有值
  • undefined 指没有值(missing value),从未赋值

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。(不要给undefined赋值!)

void运算符

表达式void ___没有返回值,因此返回结果是undefined。void并不改变表达式的结果, 只是让表达式不返回值:

var a = 42;
console.log( void a, a ); // undefined 42

如果要将代码中的值(如表达式的返回值)设为 undefined,就可以使用 void。

特殊的数字

NaN 意指“不是一个数字”(not a number),不是数字的数字,但仍然是数字类型

NaN是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即x === x不成立)的值。而 NaN != NaN 为 true,

var a = 2 / "foo"; // NaN
typeof a === "number"; // true

var a = 2 / "foo";
a == NaN;   // false
a === NaN;  // false

ES6以后,可以用Number.isNaN()判断

if (!Number.isNaN) {
    Number.isNaN = function(n) {
        return (typeof n === "number" && window.isNaN( n ));
    }
};
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b );// false——好!

// 另一种更简单的方法
if (!Number.isNaN) {
    Number.isNaN = function(n) {
        return n !== n;
    };
}

JavaScript 中Infinity(即 Number.POSITIVE_INfiNITY)表示无穷数

var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity

负零(-0)

加法和减法运算不会得到负零(negative zero)。

判断是否是负零

function isNegZero(n) {
    n = Number( n );
    return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 );// true
isNegZero( 0 / -3 );// true
isNegZero( 0 );// false

有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位 (sign)用来代表其他信息(比如移动的方向)。 此时如果一个值为 0 的变量失去了它的符号位,它的方向信息就会丢失。所以保留 0 值的符号位可以防止这类情况发生。

特殊等式

ES6 中新加入了一个工具方法 Object.is(..) 来判断两个值是否绝对相等。 仅用来判断NaN和-0等情况,能用===就不用这个

var a = 2 / "foo";
var b = -3 * 0;

Object.is( a, NaN ); // true
Object.is( b, -0 );  // true

Object.is( b, 0 );  // false

polyfill

if (!Object.is) {
    Object.is = function(v1, v2) {
        // test for `-0`
        if (v1 === 0 && v2 === 0) {
            return 1 / v1 === 1 / v2;
        }
        // test for `NaN`
        if (v1 !== v1) {
        return v2 !== v2;
        }
        // everything else
        return v1 === v2;
    };
}

值和引用

JS中,值复制还是引用复制,一切由值的类型来决定。

简单类型(null、undefined、字符串、数字、布尔和 ES6 中的 symbol)总是通过值复制方式来赋值/传递。

复合值(compound value)——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值/传递。

var a = 2;
var b = a; // `b` is always a copy of the value in `a`
b++;
a; // 2
b; // 3

var c = [1,2,3];
var d = c; // `d` is a reference to the shared `[1,2,3]` value
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]

函数传参的问题

function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]
    // 这里x变成了一个新数组,a还是原来的
    x = [4,5,6];
    x.push( 7 );
    x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [1,2,3,4]  not  [4,5,6,7]

将 a 的值变为 [4,5,6,7],必须更改 x 指向的数组,而不是为 x 赋值一个新的数组。

function foo(x) {
    x.push( 4 );
    x; // [1,2,3,4]

    // 这样做不会创建新数组
    x.length = 0; // empty existing array in-place
    x.push( 4, 5, 6, 7 );
    x; // [4,5,6,7]
}

var a = [1,2,3];

foo( a );

a; // [4,5,6,7]  not  [1,2,3,4]

如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不再是原始值。 foo( a.slice() ); a.slice()返回的是数组的浅复本,foo的操作不会影响a指向的数组。 相反,如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。

function foo(wrapper) {
    wrapper.a = 42;
}

var obj = {
    a: 2
};

foo( obj );

obj.a; // 42