- 为什么有的编程规范要求用 void 0 代替 undefined?
- 字符串有最大长度吗?
- 0.1 + 0.2 不是等于 0.3 么?为什么 JavaScript 里不是这样的?
- ES6 新加入的 Symbol 是个什么东西?
- 为什么给对象添加的方法能用在基本类型上?
类型
JavaScript 语言的每一个值都属于某一种数据类型。JavaScript 语言规定了 7 种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标准,这 7 种语言类型是:
- Undefined;
- Null;
- Boolean;
- String;
- Number;
- Symbol;
- Object。
1、Undefined、Null
为什么有的编程规范要求用 void 0 代替 undefined?
Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量 undefined(就是名为 undefined 的这个变量)来表达这个值,或者 void 运算来把任意一个表达式变成 undefined 值。
但是呢,因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取 undefined 值。
2、Boolean
Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。这个类型很简单,我就不做过多介绍了。
3、String
String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。
因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。
JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
非基本字符区域不在考虑范围之内
4、Number
JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值。
JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:
- NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字;
- Infinity,无穷大;
- -Infinity,负无穷大。
另外,值得注意的是,JavaScript 中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以 -0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。
之前说过为什么0.1+0.2!=0.3实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);、//Math.abs求绝对值
复制代码
5、Symbol
var h=new Object;
h[Symbol.iterator]=function(){
var v=10;
return{
next:function(){
return {value:v--,done:v<0}
}
}
};
for(var j of h)
console.log(j)
复制代码
代码中我们定义了 iterator 之后,用 for(var v of o) 就可以调用这个函数,然后我们可以根据函数的行为,产生一个 for…of 的行为。
这里我们给对象 o 添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个 10 到 0 的迭代器,之后我们就可以在 for of 中愉快地使用这个 o 对象了。
6、Object
Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object 表示对象的意思,它是一切有形和无形物体的总称。
为什么给对象添加的方法能用在基本类型上?
运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。
在原型上添加方法,可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了 hello 方法,在任何 Symbol 类型变量都可以调用。
Symbol.prototype.hello = () => console.log("hello");
var a = Symbol("a");
console.log(typeof a); //symbol,a 并非对象
a.hello(); //hello,有效
复制代码
类型转换
因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。
这里我们当然也不打算讲解 == 的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。
在这个里面,较为复杂的部分是 Number 和 String 之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。
1、StringToNumber
Number
多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。
2、NumberToString
String
3、装箱转换
每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。
我们可以用 console.log 看一下这个东西的 type of,它的值是 object,我们使用 symbolObject instanceof 可以看到,它是 Symbol 这个类的实例,我们找它的 constructor 也是等于 Symbol 的,所以我们无论从哪个角度看,它都是 Symbol 装箱过的对象:
var huang=(function(){return this;}).call(Symbol('a'));
console.log(typeof huangt); //object
console.log(huang instanceof Symbol); //true
console.log(huang.constructor == Symbol); //true
复制代码
装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。
var j=Object(Symbol('a'))
typeof(j) //"object"
j instanceof Symbol //true
j.constructor==Symbol //true
复制代码
var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]
复制代码
在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。
但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。
4、拆箱转换
在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。
对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// TypeError
复制代码
我们定义了一个对象 o,o 有 valueOf 和 toString 两个方法,这两个方法都返回一个对象,然后我们进行 o*2 这个运算的时候,你会看见先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。
到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 String(o),那么你会看到调用顺序就变了。
String(o)
// toString
// valueOf
// TypeError
复制代码
在 ES6 之后,还允许对象通过toPrimitive来覆盖原有的行为。
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello
复制代码
不用原生的 Number 和 parseInt,用 JS 代码实现 String 到 Number 的转换
1、string-->number
string类型 *1 即可变成 number类型
2、number-->string
number类型 +'' 即可变成 string 类型
!!!
- List 和 Record: 用于描述函数传参过程。
- Set:主要用于解释字符集等。
- Completion Record:用于描述异常、跳出等语句执行过程。
- Reference:用于描述对象属性访问、delete 等。
- Property Descriptor:用于描述对象的属性。
- Lexical Environment 和 Environment Record:用于描述变量和作用域。
- Data Block:用于描述二进制数据。
在表格中,多数项是对应的,但是请注意 object——Null 和 function——Object 是特例,我们理解类型的时候需要特别注意这个区别。