3月阅读周·你不知道的JavaScript | 值的内部特征是类型,类型定义了值的行为
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。
《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》。
当前阅读周书籍:《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》。
类型与值
类型
对语言引擎和开发人员来说,类型是值的内部特征,它定义了值的行为,以使其区别于其他值。
内置类型
JavaScript有七种内置类型:
- 空值(null)
- 未定义(undefined)
- 布尔值(boolean)
- 数字(number)
- 字符串(string)
- 对象(object)
- 符号(symbol, ES6中新增)
除对象之外,其他统称为“基本类型”。
可以用typeof运算符来查看值的类型,它返回的是类型的字符串值:
typeof undefined === 'undefined'; // true
typeof true === 'boolean'; // true
typeof 42 === 'number'; // true
typeof '42' === 'string'; // true
typeof { life: 42 } === 'object'; // true
// ES6中新加入的类型
typeof Symbol() === 'symbol'; // true
typeof null === 'object'; // true
其中null的判断bug由来已久,对于这种情况,可以通过其他方式替代:
var a = null;
!a && typeof a === 'object'; // true
此外,function(函数)、数组都是object的一个“子类型”。
值和类型
JavaScript中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。
一个变量可以现在被赋值为字符串类型值,随后又被赋值为数字类型值。
在对变量执行typeof操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型,因为JavaScript中的变量没有类型。
var a = 42;
typeof a; // "number"
a = true;
typeof a; // "boolean"
undefined和undeclared
变量在未持有值的时候为undefined。此时typeof返回"undefined":
var a;
typeof a; // "undefined"
var b = 42;
var c;
// later
b = c;
typeof b; // "undefined"
typeof c; // "undefined"
已在作用域中声明但还没有赋值的变量,是undefined的。相反,还没有在作用域中声明过的变量,是undeclared的。
var a;
a; // undefined
b; // ReferenceError: b is not defined
值
数组
在JavaScript中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的):
var a = [1, '2', [3]];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
1、类数组
有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如indexOf(..)、concat(..)、forEach(..)等)来实现。
工具函数slice(..)经常被用于这类转换:
function foo() {
var arr = Array.prototype.slice.call(arguments);
arr.push('bam');
console.log(arr);
}
foo('bar', 'baz'); // ["bar", "baz", "bam"]
用ES6中的内置工具函数Array.from(..)也能实现同样的功能:
...
var arr = Array.from( arguments );
...
字符串
JavaScript中字符串是不可变的,而数组是可变的。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push('! ');
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."
数字
1、数字的语法
JavaScript中的数字字面量一般用十进制表示。例如:
var a = 42;
var b = 42.3;
2、较小的数值
二进制浮点数最大的问题(不仅JavaScript,所有遵循IEEE 754规范的语言都是如此),是会出现如下情况:
0.1 + 0.2 === 0.3; // false
从数学角度来说,上面的条件判断应该为true,可结果为什么是false呢?
简单来说,二进制浮点数中的0.1和0.2并不是十分精确,它们相加的结果并非刚好等于0.3,而是一个比较接近的数字0.30000000000000004,所以条件判断结果为false。
3、整数的安全范围
数字的呈现方式决定了“整数”的安全值范围远远小于Number.MAX VALUE。
能够被“安全”呈现的最大整数是2^53-1,即9007199254740991,在ES6中被定义为Number.MAX SAFE INTEGER。最小整数是-9007199254740991,在ES6中被定义为Number. MIN SAFE INTEGER。
4、整数检测
要检测一个值是否是整数,可以使用ES6中的Number.isInteger(..)方法:
Number.isInteger(42); // true
Number.isInteger(42.0); // true
Number.isInteger(42.3); // false
要检测一个值是否是安全的整数,可以使用ES6中的Number.isSafeInteger(..)方法:
Number.isSafeInteger( Number.MAX SAFE INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) -1 ); // true
值和引用
JavaScript引用指向的是值。如果一个值有10个引用,这些引用指向的都是同一个值,它们相互之间没有引用/指向关系。
JavaScript对值和引用的赋值/传递在语法上没有区别,完全根据值的类型来决定:
- 简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。
- 复合值(compound value)——对象(包括数组和封装对象,参见第3章)和函数,则总是通过引用复制的方式来赋值/传递。
看下面的例子:
var a = 2;
var b = a; // b是a的值的一个复本
b++;
a; // 2
b; // 3
var c = [1, 2, 3];
var d = c; // d是[1,2,3]的一个引用
d.push(4);
c; // [1,2,3,4]
d; // [1,2,3,4]
上例中2是一个标量基本类型值,所以变量a持有该值的一个复本,b持有它的另一个复本。b更改时,a的值保持不变。
c和d则分别指向同一个复合值[1,2,3]的两个不同引用。请注意,c和d仅仅是指向值[1,2,3],并非持有。所以它们更改的是同一个值(如调用.push(4)),随后它们都指向更改后的新值[1,2,3,4]。
原生函数
常用的原生函数有:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()
实际上,它们就是内建函数。
JavaScript为基本数据类型值提供了封装对象,称为原生函数(如String、Number、Boolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim()和Array#concat(..))。
对于简单标量基本类型值,比如"abc",如果要访问它的length属性或String.prototype方法,JavaScript引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。
封装对象包装
封装对象(object wrapper)扮演着十分重要的角色。由于基本类型值没有.length和.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript会自动为基本类型值包装(box或者wrap)一个封装对象:
var a = 'abc';
a.length; // 3
a.toUpperCase(); // "ABC"
拆封
如果想要得到封装对象中的基本类型值,可以使用valueOf()函数:
var a = new String('abc');
var b = new Number(42);
var c = new Boolean(true);
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
原生函数作为构造函数
1、Array(..)
Array构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。
var a = new Array(1, 2, 3);
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
2、Date(..)和Error(..)
创建日期对象必须使用new Date()。Date(..)可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。Date(..)主要用来获得当前的Unix时间戳(从1970年1月1日开始计算,以秒为单位)。该值可以通过日期对象中的getTime()来获得。
if (!Date.now) {
Date.now = function () {
return new Date().getTime();
};
}
构造函数Error(..)带不带new关键字都可。
创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分JavaScript引擎通过只读属性.stack来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号,以便于调试(debug)。
错误对象通常与throw一起使用:
function foo(x) {
if (!x) {
throw new Error("x wasn't provided");
}
// ..
}
原生原型
原生构造函数有自己的.prototype对象,如Array.prototype、String.prototype等。
这些对象包含其对应子类型所特有的行为特征。
例如,将字符串值封装为字符串对象之后,就能访问String.prototype中定义的方法。
借助原型代理(prototype delegation),所有字符串都可以访问这些方法:
var a = ' abc ';
a.indexOf('c'); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"
总结
我们来总结一下本篇的主要内容:
- JavaScript有七种内置类型:null、undefined、boolean、number、string、object和symbol,可以使用typeof运算符来查看。变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
- null类型只有一个值null, undefined类型也只有一个值undefined。所有变量在赋值之前默认值都是undefined。void运算符返回undefined。
- 数字类型有几个特殊值,包括NaN(意指“not a number”,更确切地说是“invalid number”)、+Infinity、-Infinity和-0。
- 简单标量基本类型值(字符串和数字等)通过值复制来赋值/传递,而复合值(对象等)通过引用复制来赋值/传递。JavaScript中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值。
- JavaScript为基本数据类型值提供了封装对象,称为原生函数(如String、Number、Boolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim()和Array#concat(..))。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
若有收获,就点个赞吧
- 点赞
- 收藏
- 关注作者
评论(0)