目录
- 前言
- 正文
- 原始类型 (Primitive types)
- 原始值 (Primitive values)
- 包装对象 (Wrapper objects)
- 对象 (Object)
- 构造函数 (Constructor)
- 普通函数 (Function)
- 原始值不是对象 (Primitive not Object)
- 没有属性和方法 (No properties, no functions)
- 替身使者 (I can’t translate this)
- 总结 (Summary)
前言
随着 JavaScript 越来越流行,越来越多地开发者开始接触并使用 JavaScript。
同时我也发现,有不少开发者对于 JavaScript 最基本的原始值和包装对象都没有很清晰的理解。
那么本篇文章,就由渣皮来给大家详细介绍一下它们。
? 话不多说,Let’s go!
正文
原始类型 (Primitive types)
原始类型也被称为“基本类型”。
目前在 JavaScript 中有以下几种原始类型:
- string(字符串)
- number(数字)
- boolean(布尔)
- null(空)
- undefined(未定义)
- bigint(大整数,ES6)
- symbol(标志?ES6)
如下:
typeof 'chenpipi'; // "string" typeof 12345; // "number" typeof true; // "boolean" typeof null; // "object" typeof undefined; // "undefined" typeof 12345n; // "bigint" typeof Symbol(); // "symbol"
特别注意
typeof null 虽然返回 “object”,但是这不代表 null 就是对象,这其实是 JavaScript 的一个 Bug,且从 JavaScript 诞生以来便如此。
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 “object”。
The history of “typeof null”:https://2ality.com/2013/10/typeof-null.html
原始值 (Primitive values)
原始值也就是原始类型的值(数据)。
A primitive value is data that is not an object and has no methods.
原始值是一种没有任何方法的非对象数据。
也就是说,string、number 和 boolean 等原始类型的值本身是没有任何属性和方法的。
这个时候嗅觉敏锐的小伙伴是不是已经察觉到有什么不对劲了?
是孜然!我加了孜然!(手动狗头并划掉)
? 这里有一个非常有意思的点,但是在讨论这个问题之前,先让我们认识下包装对象。
包装对象 (Wrapper objects)
除了 null 和 undefined 外的原始类型都有其相应的包装对象:
- String(字符串)
- Number(数字)
- Boolean(布尔)
- BigInt(大整数,ES6)
- Symbol(标志?ES6)
对象 (Object)
对象是引用类型。
首先,包装对象本身是一个对象,也是函数。
String instanceof Object; // true String instanceof Function; // true
构造函数 (Constructor)
实例 (Instance)
其中 String、Number 和 Boolean 均支持使用 new 运算符来创建对应的包装对象实例。
例如 String 的声明(节选):
interface StringConstructor { new(value?: any): String; (value?: any): string; readonly prototype: String; } declare var String: StringConstructor;
使用 new 运算符得到的数据是对象(Object):
// 字符串 typeof 'pp'; // "string" typeof new String('pp'); // "object" new String() instanceof Object; // true // 数字 typeof 123; // "number" typeof new Number(123); // "object" new Number() instanceof Object; // true // 布尔 typeof true; // "boolean" typeof new Boolean(true); // "object" new Boolean() instanceof Object; // true
我们可以调用包装对象实例的 valueOf() 函数来获取其原始值:
// 字符串 let s = new String('pp'); s.valueOf(); // "pp" typeof s.valueOf(); // "string" // 数字 let n = new Number(123); n.valueOf(); // 123 typeof n.valueOf(); // "number" // 布尔 let b = new Boolean(true); b.valueOf(); // true typeof b.valueOf(); // "boolean"
“异类” (Attention)
而 BigInt 和 Symbol 都属于“不完整的类”,不支持 new 运算符。
例如 BigInt 的声明(节选):
interface BigIntConstructor { (value?: any): bigint; readonly prototype: BigInt; } declare var BigInt: BigIntConstructor;
可以看到 BigInt 的声明中没有 new 运算符相关函数。
普通函数 (Function)
包装对象也可以作为普通函数来使用。
其中 String()、Number() 和 Boolean() 函数都可以用来对任意类型的数据进行显式类型转换。
另外 Object() 函数也可用于显式类型转换,但本文不再展开。
String
示例代码:
typeof String(); // "string" String(); // "" String('pp'); // "pp" String(123); // "123" String(true); // "true" String(false); // "false" String(null); // "null" String(undefined); // "undefined" String([]); // "" String({}); // "[object Object]"
小贴士 1
当我们使用 String() 函数来转换对象时,JavaScript 会先访问对象上的 toString() 函数,如果没有实现,则会顺着原型链向上查找。
举个栗子:执行 String({ toString() { return ‘pp’; } }) 返回的结果是 “pp”,并非 “[object Object]”。
所以 String() 函数并不能够用来判断一个值是否为对象(会翻车)。
小贴士 2
常用的判断对象的方式为 Object.prototype.toString({}) === ‘[object Object]’。
举个栗子:执行 Object.prototype.toString({ toString() { return ‘pp’; } }) 返回的是 “[object Object]”。
Number
示例代码:
typeof Number(); // "number" Number(); // 0 Number(''); // 0 Number('pp'); // NaN Number(123); // 123 Number(true); // 1 Number(false); // 0 Number(null); // 0 Number(undefined); // NaN Number([]); // 0 Number({}); // NaN
小贴士
对于 Number() 函数来说,可能最实用的转换就是将 true 和 false 转换为 1 和 0 吧。
Boolean
示例代码:
typeof Boolean(); // "boolean" Boolean(); // false Boolean(''); // false Boolean('pp'); // true Boolean(0); // false Boolean(1); // true Boolean(null); // false Boolean(undefined); // false Boolean([]); // true Boolean({}); // true
小贴士
某些情况下,我们会在数据中使用 0 和 1 来表示真假状态,此时就可以使用 Boolean() 进行状态的判断。
BigInt
BigInt() 函数用于将整数转换为大整数。
该函数接受一个整数作为参数,传入参数若为浮点数或任何非数字类型数据都会报错。
示例代码:
BigInt(123); // 123n BigInt(123n); // 123n typeof 123n; // "bigint" typeof BigInt(123); // "bigint"
BigInt & Number
需要注意的是,BigInt 和 Number 是不严格相等(宽松相等)的。
示例代码:
123n === 123; // false 123n == 123; // true
Symbol
Symbol() 函数用于创建一个 symbol 类型的值。
该函数接受一个字符串作为描述符(参数),如果传入其他类型的值则会被转换为字符串(除了 undefined)。
注意,每一个 symbol 值都是独一无二的,即使它们的描述符都是一样的。
且 symbol 类型的数据只能通过 Symbol() 函数来创建。
示例代码:
// 后面的返回值是 Devtools 模拟出来的,并非实际值 Symbol('pp'); // Symbol(pp) Symbol(123); // Symbol(123) Symbol(null); // Symbol(null) Symbol({}); // Symbol([object Object]) // 类型 typeof Symbol('pp'); // "symbol" Symbol('pp') === Symbol('pp'); // false // 描述符 Symbol('pp').description; // "pp" Symbol(123).description; // "123" Symbol({}).description; // "[object Object]" Symbol().description; // undefined Symbol(undefined).description; // undefined
原始值不是对象 (Primitive not Object)
有意思的来了~
没有属性和方法 (No properties, no functions)
本文前面有提到:「原始值是一种没有任何方法的非对象数据。」
我们都知道对象(Object)上可以有属性和方法。
但是字符串不是对象,所以你不能给字符串增加属性。
做个小实验:
let a = 'chenpipi'; console.log(a.length); // 8 // 尝试增加新的属性 a.name = '吴彦祖'; console.log(a.name); // undefined // 尝试修改已有的属性 typeof a.slice; // "function" a.slice = null; typeof a.slice; // "function"
渣皮小剧场
此时一位头铁的小伙伴使用了反驳技能。
渣皮你别在这忽悠人了,我平时写 Bug 哦不写代码的时候明明可以调用到字符串、数字和布尔值上的方法!
比如下面这段代码,能够正常执行并得到符合预期的结果:
// 字符串 let s = 'chenpipi'; s.toUpperCase(); // "CHENPIPI" 'ChenPiPi'.slice(4); // "PiPi" // 数字 let n = 123; n.toString(); // "123" (123.45).toFixed(2); // "123.5" // 布尔值 let b = true; b.toString(); // "true" false.toString(); // "false"
无用小知识
有没有发现,数字的字面量后面不能直接调用函数?例如执行 123.toString() 会报 SyntaxError(语法错误)。
这是因为数字(浮点数)本身会用到小数点 .,而调用函数也需要用小数点,这时就出现了歧义(字符串和布尔值就没有这种烦恼)。
对于这种情况,我们可以使用括号 () 将数字包裹起来,如 (123).toString();或者使用两个连续的小数点 .. 来调用函数,如 123..toString()。
? 奇了怪了
那么既然字符串不是对象,那么为什么字符串会有属性和方法呢?
转念一想,数字就是数字,数字身上怎么会有方法呢?
这确实不符合逻辑,但是这又与实际相矛盾。
咋回事呢???
替身使者 (I can’t translate this)
答案揭晓~
暗中操作
以字符串(string)为例,当我们在代码中读取字符串的属性或者方法时, JavaScript 会静默地执行下面的操作:
- 将字符串通过 new String() 的方式来创建一个临时的包装对象实例;
- 通过创建的对象来执行我们的代码逻辑(读取属性或执行函数);
- 临时对象不再使用,可以被销毁。
如下面的栗子:
let a = 'chenpipi'; console.log(a); // "chenpipi" // ------------------------------ let b1 = a.length; console.log(b1); // 8 // 上面的代码相当于: let b2 = (new String(a)).length; console.log(b2); // 8 // ------------------------------ let c1 = a.toUpperCase(); console.log(c1); // "CHENPIPI" // 上面的代码相当于: let c2 = (new String(a)).toUpperCase(); console.log(c2); // "CHENPIPI"
数字(number)和布尔值(boolean)同理,但数字通过 new Number() 来创建临时对象,而布尔值则通过 new Boolean() 来创建。
除了上面的例子,最有力的证明,就是他们的构造函数:
'chenpipi'.constructor === String; // true (12345).constructor === Number; // true true.constructor === Boolean; // true
这一切都是 JavaScript 在暗中完成的,且过程中产生的临时对象都是一次性的(用完就丢)。
原来如此
芜湖,这么一来就说得通了!
这也就能解释为什么我们能够访问字符串上的属性和方法,却不能增加或修改属性。
那是因为我们实际操作的目标其实是 JavaScript 创建的临时对象,而并非字符串本身!
所以我们的增加或修改操作实际上是生效了的,只不过是在临时对象上生效了!
就像这样:
// 代码中: let a = 'chenpipi'; a.name = '吴彦祖'; console.log(a.name); // undefined // 相当于: let a = 'chenpipi'; (new String(a)).name = '吴彦祖'; console.log(a.name); // undefined // 相当于: let a = 'chenpipi'; let temp = new String(a); temp.name = '吴彦祖'; console.log(a.name); // undefined
总结 (Summary)
以上,就是本篇文章的全部内容了。
最后我们来总结一下:
- 多数原始类型都有相应的包装对象;
- 有些包装对象可以被 new,有些不行;
- 包装对象一般被用来进行显式的类型转换;
- 对象上有属性和方法;
- 原始值上没有属性和方法;
- 原始值上也不能有属性和方法;
- 但我们可以像操作对象一样来操作原始值;
- 这是因为 JavaScript 在执行代码的时候偷偷搞小动作;
- JavaScript 会用临时的包装对象来替原始值执行操作。
我们平时写代码的时候不太会注意到这件事,实际上这些也不会影响到我们写代码。
所以,这篇文章不就白看啦?
是,也不全是~
知己知彼,百战百胜。
学会以上这些无用小知识,也算是对 JavaScript 有了更深的理解了吧,至少还能用来吹牛皮(手动狗头~)。
相关资料
《JavaScript 高级程序设计(第4版)》
《JavaScript 权威指南(第6版)》
Primitive – MDN:https://developer.mozilla.org/en-US/docs/Glossary/Primitive
The history of “typeof null”:https://2ality.com/2013/10/typeof-null.html
到此这篇关于JavaScript原始值与包装对象的文章就介绍到这了,更多相关JS原始值与包装对象内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!