????????为了方便操作原始值,ECMAScript提供了3种特殊的引用类型:Boolean、Number和String。这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。来看下面的例子:
let s1 = "some text";
let s2 = s1.substring(2);
????????在这里,s1是一个包含字符串的变量,它是一个原始值。第二行紧接着在s1上调用了substring()方法,并把结果保存在s2中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操
作。具体来说,当第二行访问s1时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下3步:?
(1) 创建一个String类型的实例;
(2) 调用实例上的特定方法;
(3) 销毁实例。
可以把这3步想象成执行了如下3行ECMAScript代码:
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
????????这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上3步也会在后台发生,只不过使用的是Boolean和Number包装类型而已。?
????????引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过new实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
?????????这里的第二行代码尝试给字符串s1添加了一个color属性。可是,第三行代码访问color属性时,它却不见了。原因就是第二行代码运行时会临时创建一个String对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的String对象,但这个对象没有color属性。
????????可以显式地使用Boolean、Number和String构造函数创建原始值包装对象。不过应该在确实必要时再这么做,否则容易让开发者疑惑,分不清它们到底是原始值还是引用值。在原始值包装类型的实例上调用typeof会返回"object",所有原始值包装对象都会转换为布尔值true。
????????另外,Object构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:
let obj = new Object("some text");
console.log(obj instanceof String); // true
?????????如果传给Object的是字符串,则会创建一个String的实例。如果是数值,则会创建Number的实例。布尔值则会得到Boolean的实例。
????????注意,使用new调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:
let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number); // "number"
let obj = new Number(value); // 构造函数
console.log(typeof obj); // "object"
????????在这个例子中,变量number中保存的是一个值为25的原始数值,而变量obj中保存的是一个Number的实例。?
????????虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始值包装类型都有相应的一套方法来方便数据操作。
????????Boolean是对应布尔值的引用类型。要创建一个Boolean对象,就使用Boolean构造函数并传入true或false,如下例所示:
let booleanObject = new Boolean(true);
????????Boolean的实例会重写valueOf()方法,返回一个原始值true或false。toString()方法被调用时也会被覆盖,返回字符串"true"或"false"。不过,Boolean对象在ECMAScript中用得很少。不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用Boolean对象时,比如:
let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
let falseValue = false;
result = falseValue && true;
console.log(result); // false
?????????在这段代码中,我们创建一个值为false的Boolean对象。然后,在一个布尔表达式中通过&&操作将这个对象与一个原始值true组合起来。在布尔算术中,false && true等于false。可是,这个表达式是对falseObject对象而不是对它表示的值(false)求值。前面刚刚说过,所有对象在布尔表达式中都会自动转换为true,因此falseObject在这个表达式里实际上表示一个true值。那么true && true当然是true。
????????除此之外,原始值和引用值(Boolean对象)还有几个区别。首先,typeof操作符对原始值返回"boolean",但对引用值返回"object"。同样,Boolean对象是Boolean类型的实例,在使用instaceof操作符时返回true,但对原始值则返回false,如下所示:
console.log(typeof falseObject); // object
console.log(typeof falseValue); // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean); // false
?????????理解原始布尔值和Boolean对象之间的区别非常重要,强烈建议永远不要使用后者。
????????Number是对应数值的引用类型。要创建一个Number对象,就使用Number构造函数并传入一个数值,如下例所示:
let numberObject = new Number(10);
????????与Boolean类型一样,Number类型重写了valueOf()、toLocaleString()和toString()方法。valueOf()方法返回Number对象表示的原始数值,另外两个方法返回数值字符串。toString()方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
????????除了继承的方法,Number类型还提供了几个用于将数值格式化为字符串的方法。
????????toFixed()方法返回包含指定小数点位数的数值字符串,如:
let num = 10;
console.log(num.toFixed(2)); // "10.00"
????????这里的toFixed()方法接收了参数2,表示返回的数值字符串要包含两位小数。结果返回值为"10.00",小数位填充了0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的小数位:
let num = 10.005;
console.log(num.toFixed(2)); // "10.01"
????????toFixed()自动舍入的特点可以用于处理货币。不过要注意的是,多个浮点数值的数学计算不一定得到精确的结果。比如,0.1 + 0.2 =0.30000000000000004。
????????另一个用于格式化数值的方法是toExponential(),返回以科学记数法(也称为指数记数法)表示的数值字符串。与toFixed()一样,toExponential()也接收一个参数,表示结果中小数的位数。来看下面的例子:
let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"
????????这段代码的输出为"1.0e+1"。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数值最适当的形式,那么可以使用toPrecision()。
????????toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:
let num = 99;
console.log(num.toPrecision(1)); // "1e+2"
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
????????在这个例子中,首先要用1位数字表示数值99,得到"1e+2",也就是100。因为99不能只用1位数字来精确表示,所以这个方法就将它舍入为100,这样就可以只用1位数字(及其科学记数法形式)来表示了。用2位数字表示99得到"99",用3位数字则是"99.0"。本质上,toPrecision()方法会根据数值和精度来决定调用toFixed()还是toExponential()。为了以正确的小数位精确表示数值,这3个方法都会向上或向下舍入。
????????与Boolean对象类似,Number对象也为数值提供了重要能力。但是,考虑到两者存在同样的潜在问题,因此并不建议直接实例化Number对象。在处理原始数值和引用数值时,typeof和instacnceof操作符会返回不同的结果,如下所示:
let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false
????????原始数值在调用typeof时始终返回"number",而Number对象则返回"object"。类似地,Number对象是Number类型的实例,而原始数值不是。
????????isInteger()方法与安全整数
????????ES6新增了Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的0可能会让人误以为数值是一个浮点值:
console.log(Number.isInteger(1)); // true
console.log(Number.isInteger(1.00)); // true
console.log(Number.isInteger(1.01)); // false
????????String是对应字符串的引用类型。要创建一个String对象,使用String构造函数并传入一个数值,如下例所示:
let stringObject = new String("hello world");
????????String对象的方法可以在所有字符串原始值上调用。3个继承的方法valueOf()、toLocaleString()和toString()都返回对象的原始字符串值。
????????每个String对象都有一个length属性,表示字符串中字符的数量。来看下面的例子:
let stringValue = "hello world";
console.log(stringValue.length); // "11"
????????这个例子输出了字符串"hello world"中包含的字符数量:11。注意,即使字符串中包含双字节字符(而不是单字节的ASCII字符),也仍然会按单字符来计数。
????????String类型提供了很多方法来解析和操作字符串。
????????JavaScript字符串由16位码元(code unit)组成。对多数字符来说,每16位码元对应一个字符。换句话说,字符串的length属性表示字符串包含多少16位码元:
let message = "abcde";
console.log(message.length); // 5
????????此外,charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的16位码元,并返回该码元对应的字符:
let message = "abcde";
console.log(message.charAt(2)); // "c"
????????JavaScript字符串使用了两种Unicode编码混合的策略:UCS-2和UTF-16。对于可以采用16位编码的字符(U+0000~U+FFFF),这两种编码实际上是一样的。
????????使用charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。比如:
let message = "abcde";
// Unicode "Latin small letter C"的编码是U+0063
console.log(message.charCodeAt(2)); // 99
// 十进制99等于十六进制63
console.log(99 === 0x63); // true
?????????fromCharCode()方法用于根据给定的UTF-16码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:
// Unicode "Latin small letter A"的编码是U+0061
// Unicode "Latin small letter B"的编码是U+0062
// Unicode "Latin small letter C"的编码是U+0063
// Unicode "Latin small letter D"的编码是U+0064
// Unicode "Latin small letter E"的编码是U+0065
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)); // "abcde"
// 0x0061 === 97
// 0x0062 === 98
// 0x0063 === 99
// 0x0064 === 100
// 0x0065 === 101
console.log(String.fromCharCode(97, 98, 99, 100, 101)); // "abcde"
?????????对于U+0000~U+FFFF范围内的字符,length、charAt()、charCodeAt()和fromCharCode()返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16位表示的,而这几个方法也都基于16位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。
????????这个对应关系在扩展到Unicode增补字符平面时就不成立了。问题很简单,即16位只能唯一表示65 536个字符。这对于大多数语言字符集是足够了,在Unicode中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符使用两个16位码元的策略称为代理对。
????????在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:
// "smiling face with smiling eyes" 表情符号的编码是U+1F60A
// 0x1F60A === 128522
let message = "ab?de";
console.log(message.length); // 6
console.log(message.charAt(1)); // b
console.log(message.charAt(2)); // <?>
console.log(message.charAt(3)); // <?>
console.log(message.charAt(4)); // d
console.log(message.charCodeAt(1)); // 98
console.log(message.charCodeAt(2)); // 55357
console.log(message.charCodeAt(3)); // 56842
console.log(message.charCodeAt(4)); // 100
console.log(String.fromCodePoint(0x1F60A)); // ?
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de
????????这些方法仍然将16位码元当作一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个Unicode笑脸字符。
????????为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用codePointAt()来代替charCodeAt()。跟使用charCodeAt()时类似,codePointAt()接收16位码元的索引并返回该索引位置上的码点(code point)。码点是Unicode中一个字符的完整标识。比如,"c"的码点是0x0063,而"?"的码点是0x1F60A。码点可能是16位,也可能是32位,而codePointAt()方法可以从指定码元位置识别完整的码点。
let message = "ab?de";
console.log(message.codePointAt(1)); // 98
console.log(message.codePointAt(2)); // 128522
console.log(message.codePointAt(3)); // 56842
console.log(message.codePointAt(4)); // 100
????????注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:
console.log([..."ab?de"]); // ["a", "b", "?", "d", "e"]
????????与charCodeAt()有对应的codePointAt()一样,fromCharCode()也有一个对应的fromCodePoint()。这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de
console.log(String.fromCodePoint(97, 98, 128522, 100, 101)); // ab?de
????????某些Unicode字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示。比如:
// U+00C5:上面带圆圈的大写拉丁字母A
console.log(String.fromCharCode(0x00C5)); // ?
// U+212B:长度单位“埃”
console.log(String.fromCharCode(0x212B)); // ?
// U+004:大写拉丁字母A
// U+030A:上面加个圆圈
console.log(String.fromCharCode(0x0041, 0x030A)); // ?
?????????比较操作符不在乎字符看起来是什么样的,因此这3个字符互不相等。
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(a1 === a3); // false
console.log(a2 === a3); // false
?????????为解决这个问题,Unicode提供了4种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。这4种规范化形式是:NFD(Normalization Form D)、
NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。可以使用normalize()方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。
????????通过比较字符串与其调用normalize()的返回值,就可以知道该字符串是否已经规范化了:
let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
// 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
// U+212B是未规范化的
console.log(a2 === a2.normalize("NFD")); // false
console.log(a2 === a2.normalize("NFC")); // false
console.log(a2 === a2.normalize("NFKD")); // false
console.log(a2 === a2.normalize("NFKC")); // false
// U+0041/U+030A是对0+212B进行NFD/NFKD规范化之后的结果
console.log(a3 === a3.normalize("NFD")); // true
console.log(a3 === a3.normalize("NFC")); // false
console.log(a3 === a3.normalize("NFKD")); // true
console.log(a3 === a3.normalize("NFKC")); // false
????????选择同一种规范化形式可以让比较操作符返回正确的结果:?
let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1.normalize("NFD") === a2.normalize("NFD")); // true
console.log(a2.normalize("NFKC") === a3.normalize("NFKC")); // true
console.log(a1.normalize("NFC") === a3.normalize("NFC")); // true
????????本节介绍几个操作字符串值的方法。首先是concat(),用于将一个或多个字符串拼接成一个新字符串。来看下面的例子:
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"
????????在这个例子中,对stringValue调用concat()方法的结果是得到"hello world",但stringValue的值保持不变。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串,如下所示:
let stringValue = "hello ";
let result = stringValue.concat("world", "!");
console.log(result); // "hello world!"
console.log(stringValue); // "hello"
????????这个修改后的例子将字符串"world"和"!"追加到了"hello "后面。虽然concat()方法可以拼接字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。
????????ECMAScript提供了3个从字符串中提取子字符串的方法:slice()、substr()和substring()。这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。对slice()和substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对substr()而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。与concat()方法一样,slice()、substr()和substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。来看下面的例子:
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
????????在这个例子中,slice()、substr()和substring()是以相同方式被调用的,而且多数情况下返回的值也相同。如果只传一个参数3,则所有方法都将返回"lo world",因为"hello"中"l"位置为3。如果
传入两个参数3和7,则slice()和substring()返回"lo w"(因为"world"中"o"在位置7,不包含),而substr()返回"lo worl",因为第二个参数对它而言表示返回的字符数。
????????当某个参数是负值时,这3个方法的行为又有不同。比如,slice()方法将所有负值参数都当成字符串长度加上负参数值。
????????而substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为0。substring()方法会将所有负参数值都转换为0。看下面的例子:
let stringValue = "hello world";
console.log(stringValue.slice(-3)); // "rld"
console.log(stringValue.substring(-3)); // "hello world"
console.log(stringValue.substr(-3)); // "rld"
console.log(stringValue.slice(3, -4)); // "lo w"
console.log(stringValue.substring(3, -4)); // "hel"
console.log(stringValue.substr(3, -4)); // "" (empty string)
????????这个例子明确演示了3个方法的差异。在给slice()和substr()传入负参数时,它们的返回结果相同。这是因为-3会被转换为8(长度加上负参数),实际上调用的是slice(8)和substr(8)。而substring()方法返回整个字符串,因为-3会转换为0。
????????在第二个参数是负值时,这3个方法各不相同。slice()方法将第二个参数转换为7,实际上相当于调用slice(3, 7),因此返回"low"。而substring()方法会将第二个参数转换为0,相当于调
用substring(3, 0),等价于substring(0, 3),这是因为这个方法会将较小的参数作为起点,将较大的参数作为终点。对substr()来说,第二个参数会被转换为0,意味着返回的字符串包含零个字符,因而会返回一个空字符串。
未完待续。。。
????????ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括Object、Array和String。本节介绍ECMA-262定义的另外两个单例内置对象:Global和Math。
????????Global对象是ECMAScript中最特别的对象,因为代码不会显式地访问它。ECMA-262规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性 。本书前面介绍的函数,包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global对象的方法。除了这些,Global对象上还有另外一些方法。
????????encodeURI()和encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。有效的URI不能包含某些字符,比如空格。使用URI编码方法来编码URI可以让浏览器能够理解它们,同时又以特殊的UTF-8编码替换掉所有无效字符。
????????ecnodeURI()方法用于对整个URI进行编码,比如"www.wrox.com/illegal value.js"。而encodeURIComponent()方法用于编码URI中单独的组件,比如前面URL中的"illegal value.js"。这两个方法的主要区别是,encodeURI()不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent()会编码它发现的所有非标准字符。来看下面的例子:
let uri = "http://www.wrox.com/illegal value.js#start";
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));
????????这里使用encodeURI()编码后,除空格被替换为%20之外,没有任何变化。而encodeURIComponent()方法将所有非字母字符都替换成了相应的编码形式。这就是使用encodeURI()编码整个URI,但只使用encodeURIComponent()编码那些会追加到已有URI后面的字符串的原因。
注意 ????????一般来说,使用encodeURIComponent()应该比使用encodeURI()的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。
????????与encodeURI()和encodeURIComponent()相对的是decodeURI()和decodeURIComponent()。decodeURI()只对使用encodeURI()编码过的字符解码。例如,%20会被替换为空格,但%23不会被替换为井号(#),因为井号不是由encodeURI()替换的。类似地,decodeURIComponent()解码所有被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。来看下面的例子:
let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start";
// http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start
console.log(decodeURI(uri));
// http:// www.wrox.com/illegal value.js#start
console.log(decodeURIComponent(uri));
????????这里,uri变量中包含一个使用encodeURIComponent()编码过的字符串。首先输出的是使用decodeURI()解码的结果,可以看到只用空格替换了%20。然后是使用decodeURIComponent()解码的结果,其中替换了所有特殊字符,并输出了没有包含任何转义的字符串。(这个字符串不是有效的URL。)
注意 ????????URI方法encodeURI()、encodeURIComponent()、decodeURI()和
decodeURIComponent()取代了escape()和unescape()方法,后者在ECMA-262第3版中就已经废弃了。URI方法始终是首选方法,因为它们对所有Unicode字符进行编码,而原来的方法只能正确编码ASCII字符。不要在生产环境中使用escape()和unescape()。
????????最后一个方法可能是整个ECMAScript语言中最强大的了,它就是eval()。这个方法就是一个完整的ECMAScript解释器,它接收一个参数,即一个要执行的ECMAScript(JavaScript)字符串。来看一个例子:
eval("console.log('hi')");
?上面这行代码的功能与下一行等价:
console.log("hi");
????????当解释器发现eval()调用时,会将参数解释为实际的ECMAScript语句,然后将其插入到该位置。通过eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在eval()调用内部被引用,比如下面这个例子:
let msg = "hello world";
eval("console.log(msg)"); // "hello world"
????????这里,变量msg是在eval()调用的外部上下文中定义的,而console.log()显示了文本"hello world"。这是因为第二行代码会被替换成一行真正的函数调用代码。类似地,可以在eval()内部
定义一个函数或变量,然后在外部代码中引用,如下所示:? ? ?
eval("function sayHi() { console.log('hi'); }");
sayHi();
?????????这里,函数sayHi()是在eval()内部定义的。因为该调用会被替换为真正的函数定义,所以才可能在下一行代码中调用sayHi()。对于变量也是一样的:
eval("let msg = 'hello world';");
console.log(msg); // Reference Error: msg is not defined
????????通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在eval()执行的时候才会被创建。
msg是一个普通变量,它的作用域是eval()函数的局部作用域,eval()执行完毕后msg就被销毁了,无法在外部被访问。而sayHi函数是一种特殊的对象,eval()执行完毕后它不会被立即销毁,而是会在垃圾回收机制运行并将其销毁之前一直存在在内存中,所以可以在外部访问
????????通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在eval()执行的时候才会被创建。
????????在严格模式下,在eval()内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报错。同样,在严格模式下,赋值给eval也会导致错误:?
"use strict";
eval = "hi"; // 导致错误
注意 ????????解释代码字符串的能力是非常强大的,但也非常危险。在使用eval()的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对XSS利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。
????????Global对象有很多属性,其中一些前面已经提到过了。像undefined、NaN和Infinity等特殊值都是Global对象的属性。此外,所有原生引用类型构造函数,比如Object和Function,也都是Global对象的属性。下表列出了所有这些属性。
?????????虽然ECMA-262没有规定直接访问Global对象的方式,但浏览器将window对象实现为Global对象的代理。因此,所有全局作用域中声明的变量和函数都变成了window的属性。来看下面的例子:
var color = "red";
function sayColor() {
console.log(window.color);
}
window.sayColor(); // "red"
????????这里定义了一个名为color的全局变量和一个名为sayColor()的全局函数。在sayColor()内部,通过window.color访问了color变量,说明全局变量变成了window的属性。接着,又通过window对象直接调用了window.sayColor()函数,从而输出字符串。
????????另一种获取Global对象的方式是使用如下的代码:
let global = function() {
return this;
}();
????????这段代码创建一个立即调用的函数表达式,返回了this的值。如前所述,当一个函数在没有明确(通过成为某个对象的方法,或者通过call()/apply())指定this值的情况下执行时,this值等于Global对象。因此,调用一个简单返回this的函数是在任何执行上下文中获取Global对象的通用方式。
????????ECMAScript提供了Math对象作为保存数学公式、信息和计算的地方。Math对象提供了一些辅助计算的属性和方法。
注意 ????????Math对象上提供的计算要比直接在JavaScript实现的快得多,因为Math对象上的计算使用了JavaScript引擎中更高效的实现和处理器指令。但使用Math计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。
????????Math对象有一些属性,主要用于保存数学中的一些特殊值。下表列出了这些属性。
????????Math对象也提供了很多辅助执行简单或复杂数学计算的方法。min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数,如下面的例子所示:
let max = Math.max(3, 54, 32, 16);
console.log(max); // 54
let min = Math.min(3, 54, 32, 16);
console.log(min); // 3
????????在3、54、32和16中,Math.max()返回54,Math.min()返回3。使用这两个方法可以避免使用额外的循环和if语句来确定一组数值的最大最小值。
????????要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符:
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...val);
????????接下来是用于把小数值舍入为整数的4个方法:Math.ceil()、Math.floor()、Math.round()和Math.fround()。这几个方法处理舍入的方式如下所述。
????????以下示例展示了这些方法的用法:
console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26
console.log(Math.round(25.9)); // 26
console.log(Math.round(25.5)); // 26
console.log(Math.round(25.1)); // 25
console.log(Math.fround(0.4)); // 0.4000000059604645
console.log(Math.fround(0.5)); // 0.5
console.log(Math.fround(25.9)); // 25.899999618530273
console.log(Math.floor(25.9)); // 25
console.log(Math.floor(25.5)); // 25
console.log(Math.floor(25.1)); // 25
????????对于25和26(不包含)之间的所有值,Math.ceil()都会返回26,因为它始终向上舍入。Math.round()只在数值大于等于25.5时返回26,否则返回25。最后,Math.floor()对所有25和26(不包含)之间的值都返回25。
????????Math.random()方法返回一个0~1范围内的随机数,其中包含0但不包含1。对于希望显示随机名言或随机新闻的网页,这个方法是非常方便的。可以基于如下公式使用Math.random()从一组整数中随机选择一个数:
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value
????????这里使用了Math.floor()方法,因为Math.random()始终返回小数,即便乘以一个数再加上一个数也是小数。因此,如果想从1~10范围内随机选择一个数,代码就是这样的:
let num = Math.floor(Math.random() * 10 + 1);
????????这样就有10个可能的值(1~10),其中最小的值是1。如果想选择一个2~10范围内的值,则代码就要写成这样:
let num = Math.floor(Math.random() * 9 + 2);
????????2~10只有9个数,所以可选总数(total_number_of_choices)是9,而最小可能的值(first_possible_value)是2。很多时候,通过函数来算出可选总数和最小可能的值可能更方便,比如:
function selectFrom(lowerValue, upperValue) {
let choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
let num = selectFrom(2,10);
console.log(num); // 2~10范围内的值,其中包含2和10
????????这里的函数selectFrom()接收两个参数:应该返回的最小值和最大值。通过将这两个值相减再加1得到可选总数,然后再套用上面的公式。于是,调用selectFrom(2,10)就可以从2~10(包含)范围内选择一个值了。使用这个函数,从一个数组中随机选择一个元素就很容易,比如:
let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
let color = colors[selectFrom(0, colors.length-1)];
????????在这个例子中,传给selecFrom()的第二个参数是数组长度减1,即数组最大的索引值。
注意 ????????Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用window.crypto.getRandomValues()。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
????????JavaScript中的对象称为引用值,几种内置的引用类型可用于创建特定类型的对象。
????????JavaScript比较独特的一点是,函数实际上是Function类型的实例,也就是说函数也是对象。因为函数也是对象,所以函数也有方法,可以用于增强其能力。
????????由于原始值包装类型的存在,JavaScript中的原始值可以被当成对象来使用。有3种原始值包装类型:Boolean、Number和String。它们都具备如下特点。
????????当代码开始执行时,全局上下文中会存在两个内置对象:Global和Math。其中,Global对象在大多数ECMAScript实现中无法直接访问。不过,浏览器将其实现为window对象。所有全局变量和函数都是Global对象的属性。Math对象包含辅助完成复杂计算的属性和方法。