阅读周·你不知道的JavaScript | 值的内部特征是类型,类型定义了值的行为

时间:2024-10-09 13:27:22

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。

《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》

当前阅读周书籍《你不知道的JavaScript(上卷)》、《你不知道的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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞???? | 收藏⭐️ | 留言????。

若有收获,就点个赞吧