js数据类型你真的懂了么?
js数据类型有哪些?这个很基础的问题,被问到的人可能会想干嘛拿这么简单的弱智问题问我,这怎么会问倒我,不就是那几种么,但是深入下去我们经常会犯一些错误,下面会详细介绍js数据类型及常见的特殊情况,还有如何判断数据类型,数据类型之间如何转换等
文章目录
- js数据类型你真的懂了么?
- 原始(Primitive)类型
- 对象(Object)类型
- 类型数据判断 typeof vs instanceof vs constructor vs Object.prototype.toString
- 类型转换
- 关于红宝书第四版里面的关于数据类型的梳理-3.2数据类型
- 3.2.1 typeof操作符
- 3.2.2 Number类型
- 3.2.3 String类型
- 3.2.4 Symbol类型
- 3.2.5 Object类型
原始(Primitive)类型
原始类型有哪几种?null 是对象嘛?
在JS中,存在着7的原始(Primitive)类型,分别是:
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ES6)
- BigInt (ES10)
首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()
//在控制台打印
undefined.toString()
//输出结果
VM703:1 Uncaught TypeError: Cannot read property 'toString' of undefined
at <anonymous>:1:11
此时你肯定会有疑问,这不对呀,明明 ‘1’.toString() 是可以使用的。其实在这种情况下,‘1’ 已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,所以可以调用 toString 函数。
除了会在必要的情况下强转类型以外,原始类型还有一些坑。
其中 JS 的 number 类型是浮点类型的,在使用中会遇到某些 Bug,比如 0.1 + 0.2 ! 0.3,但是这一块的内容会在进阶部分讲到。string 类型是不可变的,无论你在 string 类型上调用何种方法,都不会对值有改变。
另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
BigInt重点说一下这个新的原始类型
BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。
可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()。
在以往的版本中,我们有以下的弊端:
// 大于2的53次方的整数,无法保持精度
2 ** 53 === (2 ** 53 + 1)
// 超过2的1024次方的数值,无法表示
2 ** 1024 // Infinity
但是在ES10引入BigInt之后,这个问题便得到了解决。
以下操作符可以和 BigInt 一起使用: +、*、-、**、% 。除 >>> (无符号右移)之外的位操作也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigInt。BigInt 不支持单目 (+) 运算符。
/ 操作符对于整数的运算也没问题。可是因为这些变量是 BigInt 而不是 BigDecimal ,该操作符结果会向零取整,也就是说不会返回小数部分。
BigInt 和 Number不是严格相等的,但是宽松相等的。
对象(Object)类型
对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
const a = []
对于常量 a 来说,假设内存地址(指针)为 #001,那么在地址 #001 的位置存放了值 [],常量 a 存放了地址(指针) #001,再看以下代码
const a = []
const b = a
b.push(1)
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #001,当我们进行数据修改的时候,就会修改存放在地址(指针) #001 上的值,也就导致了两个变量的值都发生了改变。
接下来我们来看函数参数是对象的情况
function test(person) {
person.age = 26
person = {
name: 'xyl',
age: 27
}
return person
}
const p1 = {
name: 'dxx',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
对于以上代码,你是否能正确的写出结果呢?接下来让我为你解析一番:
//输出结果如下
{name: "dxx", age: 26}
{name: "xyl", age: 27}
首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了
但是当我们重新为 person 分配了一个对象时就出现了分歧,请看下图
所以最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,导致了最终两个变量的值是不相同的。
类型数据判断 typeof vs instanceof vs constructor vs Object.prototype.toString
1.typeof:
可以对基本类型做出准确的判断,但对于引用类型,用它就有点力不从心了
typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么
typeof 返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function等6种数据类型。
typeof 可以对JS基本数据类型做出准确的判断(除了null),而对于引用类型返回的基本上都是object, 其实返回object也没有错,因为所有对象的原型链最终都指向了Object,Object是所有对象的祖宗
。 但当我们需要知道某个对象的具体类型时,typeof 就显得有些力不从心了。
注意:typeof null会返回object,因为特殊值null被认为是一个空的对象引用
判断 Target 的类型,单单用 typeof 并无法完全满足,这其实并不是 bug,本质原因是 JS 的万物皆对象的理论。因此要真正完美判断时,我们需要区分对待:
- 基本类型(null): 使用 String(null)
- 基本类型(string / number / boolean / undefined) + function: 直接使用 typeof即可
- 其余引用类型(Array / Date / RegExp Error): 调用toString后根据[object XXX]进行判断
很稳的判断封装:
let class2type = {}
'Array Date RegExp Object Error'.split(' ').forEach(e => class2type[ '[object ' + e + ']' ] = e.toLowerCase())
function type(obj) {
if (obj == null) return String(obj)
return typeof obj === 'object' ? class2type[ Object.prototype.toString.call(obj) ] || 'object' : typeof obj
}
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // "object"
typeof 2 ** 1024 //Uncaught SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
2.instanceof
判断对象和构造函数在原型链上是否有关系,如果有关系,返回真,否则返回假
如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的,在后面的章节中我们也会自己去实现一个 instanceof。
function Aaa(){
}
var a1 = new Aaa();
console.log( a1 instanceof Aaa); //true判断a1和Aaa是否在同一个原型链上,是的话返回真,否则返回假
var arr = [];
console.log( arr instanceof Aaa);//false
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
我们再来看:
var str = 'hello';
console.log(str instanceof String);//false
var bool = true;
console.log(bool instanceof Boolean);//false
var num = 123;
console.log(num instanceof Number);//false
var nul = null;
console.log(nul instanceof Object);//false
var und = undefined;
console.log(und instanceof Object);//false
var oDate = new Date();
console.log(oDate instanceof Date);//true
var json = {};
console.log(json instanceof Object);//true
var arr = [];
console.log(arr instanceof Array);//true
var reg = /a/;
console.log(reg instanceof RegExp);//true
var fun = function(){};
console.log(fun instanceof Function);//true
var error = new Error();
console.log(error instanceof Error);//true
从上面的运行结果我们可以看到,基本数据类型是没有检测出他们的类型,但是我们使用下面的方式创建num、str、boolean,是可以检测出类型的:
var num = new Number(123);
var str = new String('abcdef');
var bo = new Boolean(true);
console.log(num instanceof Number);//true
console.log(str instanceof String);//true
console.log(bo instanceof Boolean);//true
当然我们还是有办法让 instanceof 判断原始类型的
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的东西,以上代码等同于 typeof ‘hello world’ = ‘string’,所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
instanceof原理
能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:
// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype
// return true
3.constructor
查看对象对应的构造函数
constructor 在其对应对象的原型下面,是自动生成的。当我们写一个构造函数的时候,程序会自动添加:构造函数名.prototype.constructor = 构造函数名
function Aaa(){
}
//Aaa.prototype.constructor = Aaa; //每一个函数都会有的,都是自动生成的
//Aaa.prototype.constructor = Aaa;
判断数据类型的方法
var str = 'hello';
console.log(str.constructor == String);//true
var bool = true;
console.log(bool.constructor == Boolean);//true
var num = 123;
console.log(num.constructor ==Number);//true
// var nul = null;
// console.log(nul.constructor == Object);//报错
//var und = undefined;
//console.log(und.constructor == Object);//报错
var oDate = new Date();
console.log(oDate.constructor == Date);//true
var json = {};
console.log(json.constructor == Object);//true
var arr = [];
console.log(arr.constructor == Array);//true
var reg = /a/;
console.log(reg.constructor == RegExp);//true
var fun = function(){};
console.log(fun.constructor ==Function);//true
var error = new Error();
console.log(error.constructor == Error);//true
从上面的测试中我们可以看到,undefined和null是不能够判断出类型的,并且会报错。因为null和undefined是无效的对象,因此是不会有constructor存在的
同时我们也需要注意到的是:使用constructor是不保险的,因为constructor属性是可以被修改的,会导致检测出的结果不正确
可以看出,constructor并没有正确检测出正确的构造函数
4.Object.prototype.toString
可以说不管是什么类型,它都可以立即判断出
toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型
格式为[object xxx],xxx是具体的数据类型,其中包括:
String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,… 基本上所有对象的类型都可以通过这个方法获取到。
var str = 'hello';
console.log(Object.prototype.toString.call(str));//[object String]
var bool = true;
console.log(Object.prototype.toString.call(bool))//[object Boolean]
var num = 123;
console.log(Object.prototype.toString.call(num));//[object Number]
var nul = null;
console.log(Object.prototype.toString.call(nul));//[object Null]
var und = undefined;
console.log(Object.prototype.toString.call(und));//[object Undefined]
var oDate = new Date();
console.log(Object.prototype.toString.call(oDate));//[object Date]
var json = {};
console.log(Object.prototype.toString.call(json));//[object Object]
var arr = [];
console.log(Object.prototype.toString.call(arr));//[object Array]
var reg = /a/;
console.log(Object.prototype.toString.call(reg));//[object RegExp]
var fun = function(){};
console.log(Object.prototype.toString.call(fun));//[object Function]
var error = new Error();
console.log(Object.prototype.toString.call(error));//[object Error]
从这个结果也可以看出,不管是什么类型的,Object.prototype.toString.call();都可以判断出其具体的类型。
接下来我们分析一下四种方法各自的优缺点
5.四种判断数据类型方法对比
不同类型的优缺点 | typeof | instanceof | constructor | Object.prototype.toString.call |
---|---|---|---|---|
优点 | 使用简单 | 能检测出引用类型 | 基本能检测所有的类型(除了null和undefined) | 检测出所有的类型 |
缺点 | 只能检测出基本类型(出null) | 不能检测出基本类型,且不能跨iframe | constructor易被修改,也不能跨iframe | IE6下,undefined和null均为Object |
从上表中我们看到了,instanceof和constructor不能跨iframe,上面没有细说,所以下面我们直接上例子喽
例:跨页面判断是否是数组
window.onload = function(){
var oF = document.createElement('iframe');
document.body.appendChild( oF );
var ifArray = window.frames[0].Array;
var arr = new ifArray();
//alert( arr.constructor == Array ); //false
//alert( arr instanceof Array ); //false
alert( Object.prototype.toString.call(arr) == '[object Array]' ); //true
};
类型转换
首先我们要知道,在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
JS 中在使用运算符号或者对比符时,会自带隐式转换,规则如下:
-、*、/、% :一律转换成数值后计算
+:
- 数字 + 字符串 = 字符串, 运算顺序是从左到右
- 数字 + 对象, 优先调用对象的valueOf -> toString
- 数字 + boolean/null -> 数字
- 数字 + undefined -> NaN
[1].toString() === ‘1’
{}.toString() === ‘[object object]’
NaN !== NaN 、+undefined 为 NaN
转Boolean
在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。
对象转原始类型
对象在转换类型的时候,会调用内置的[[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 如果需要转字符串类型就调用 x.toString(),转换为基础类型的话就返回转换的值。不是字符串类型的话就先调用 valueOf,结果不是基础类型的话再调用 toString
- 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
- 如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
四则运算符
加法运算符不同于其他几个运算符,它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
如果你对于答案有疑问的话,请看解析:
- 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 ‘11’
- 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
- 对于第三行代码来说,触发特点二,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3
另外对于加法还需要注意这个表达式 ‘a’ + + 'b’
‘a’ + + ‘b’ // -> “aNaN”
因为 + ‘b’ 等于 NaN,所以结果为 “aNaN”,你可能也会在一些代码中看到过 + ‘1’ 的形式来快速获取 number 类型。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
- 如果是对象,就通过 toPrimitive 转换对象
- 如果是字符串,就通过 unicode 字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。
上面总结的那些应付面试应该是没啥问题,不过作为一个总结梳理搬运工,我整理了JavaScripts高级程序设计(第四版)中第三章最新的关于数据类型的整理介绍,{如果你觉得的上面整理的不够详细可看下面|万 一 面 试 官 问 的 很 细 呢}↓详情!
关于红宝书第四版里面的关于数据类型的梳理-3.2数据类型
ECMAScript有6种简单数据类型(也称为原始类型):Undefined
、Null
、Boolean
、Number
、String
和Symbol
。Symbol
(符号)是ECMAScript 6新增的。还有一种复杂数据类型叫Object
(对象)。
3.2.1 typeof操作符
"undefined"
表示值未定义;"boolean"
表示值为布尔值;"string"
表示值为字符串;"number"
表示值为数值;"object"
表示值为对象(而不是函数)或null
;"function"
表示值为函数;"symbol"
表示值为符号。
下面是使用typeof
操作符的例子:
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
let dxx;
console.log(typeof dxx); // "undefined"
//let age;
console.log(typeof age); // "undefined"
let car = null;
console.log(typeof car); // "object"
console.log(null == undefined); // true
3.2.2 Number类型
数据类型 | 转换为true 的值 | 转换为false 的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | "" (空字符串) |
Number | 非零数值(包括无穷值) | 0 、NaN (参见后面的相关内容) |
Object | 任意对象 | null |
Undefined | N/A (不存在) | undefined |
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}
这里检测两个数值之和是否等于0.3。如果两个数值分别是0.05和0.25,或者0.15和0.15,那没问题。但如果是0.1和0.2,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。
console.log(0/0); // NaN
console.log(-0/+0); // NaN
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
//NaN有几个独特的属性。首先,任何涉及NaN的操作始终返回NaN(如NaN/10),在连续多步计算时这可能是个问题。其次,NaN不等于包括NaN在内的任何值。例如,下面的比较操作会返回false:
console.log(NaN == NaN); // false
// 任何不能转换为数值的值都会导致这个函数返回true
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10是数值
console.log(isNaN("10")); // false,可以转换为数值10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值1
数值转换
有3个函数可以将非数值转换为数值:Number()
、parseInt()
和parseFloat()
。Number()
是转型函数,可用于任何数据类型。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
let num5 = Number(false); // 0
let num6 = Number(null); // 0
let num7 = Number(undefined); // NaN
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
let num = parseInt("0xAF", 16); // 175 ,parseInt()也接收第二个参数,用于指定底数(进制数)
let num1 = parseInt("AF", 16); // 175 ,
let num2 = parseInt("AF"); // NaN
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
//十六进制数值始终会返回0。因为parseFloat()只解析十进制值,因此不能指定底数。
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
3.2.3 String类型
let text = "This is the letter sigma: \u03a3.";
console.log(text.length); // 28
注意 如果字符串中包含双字节字符,那么length
属性返回的值可能不是准确的字符数。第5章将具体讨论如何解决这个问题。
toString() VS String()
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
//null和undefined值没有toString()方法
let mm = undefined;
//let mm = null;
console.log(mm.toString());//VM75687:2 Uncaught TypeError: Cannot read property 'toString' of undefined
//Uncaught TypeError: Cannot read property 'toString' of null
//toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,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"
String()
/**
*如果你不确定一个值是不是null或undefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。
*如果值有toString()方法,则调用该方法(不传参数)并返回结果。
*如果值是null,返回"null"。
*如果值是undefined,返回"undefined"。
**/
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
模板字面量
ECMAScript 6新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
//顾名思义,模板字面量在定义模板时特别有用,比如下面这个HTML模板:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
//由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串可能会看起来缩进不当:
// 这个模板字面量在换行符之后有25个空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
// 这个模板字面量以一个换行符开头
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true
// 这个模板字面量没有意料之外的字符
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral[0]);
// first line
// second line
字符串插值
字符串插值通过在${}
中使用一个JavaScript表达式实现:
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
// 所有插入的值都会使用toString()强制转型为字符串,而且任何JavaScript表达式都可以用于插值。嵌套的模板字符串无须转义:
console.log(`Hello, ${ `World` }!`); // Hello, World!
//将表达式转换为字符串时会调用toString():
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
//在插值表达式中可以调用函数和方法:
function capitalize(word) {
return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
//此外,模板也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc
模板字面量标签函数
模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。
标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
/**因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个数组中:**/
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
/**
对于有n个插值的模板字面量,传给标签函数的表达式参数的个数始终是n,而传给标签函数的第一个参数所包含的字符串个数则始终是n+1。因此,如果你想把这些字符串和对表达式求值的结果拼接起来作为默认返回的字符串,可以这样做:
**/
let a = 6;
let b = 9;
function zipTag(strings, ...expressions) {
return strings[0] +
expressions.map((e, i) => `${e}${strings[i + 1]}`)
.join('');
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = zipTag`${ a } + ${ b } = ${ a + b }`;
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "6 + 9 = 15"
原始字符串
-
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或Unicode字符),而不是被转换后的字符表示。为此,可以使用默认的
String.raw
标签函数:// Unicode示例 // \u00A9是版权符号 console.log(`\u00A9`); // © console.log(String.raw`\u00A9`); // \u00A9 // 换行符示例 console.log(`first line\nsecond line`); // first line // second line console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" // 对实际的换行符来说是不行的 // 它们不会被转换成转义序列的形式 console.log(`first line second line`); // first line // second line console.log(String.raw`first line second line`); // first line // second line
另外,也可以通过标签函数的第一个参数,即字符串数组的
.raw
属性取得每个字符串的原始内容:function printRaw(strings) { console.log('Actual characters:'); for (const string of strings) { console.log(string); } console.log('Escaped characters;'); for (const rawString of strings.raw) { console.log(rawString); } } printRaw`\u00A9${ 'and' }\n`; // Actual characters: // © //(换行符) // Escaped characters: // \u00A9 // \n
3.2.4 Symbol类型
symbol 英文意思为 符号、象征、标记、记号,在 js 中更确切的翻译应该为 独一无二的值,理解了它的意思之后再看起来就简单多了。
可以通过下面的方式来创建一个 symbol 类型的值
const s = Symbol();
console.log(typeof s); // "symbol"
需要注意的是通过 Symbol 方法创建值的时候不用使用 new 操作符,原因是通过 new 实例化的结果是一个 object 对象,而不是原始类型的 symbol
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
Symbol 方法接收一个参数,表示对生成的 symbol 值的一种描述
const s = Symbol('foo');
即使是传入相同的参数,生成的 symbol 值也是不相等的,因为 Symbol 本来就是独一无二的意思
const foo = Symbol('foo');
const bar = Symbol('foo');
console.log(foo === bar); // false
Symbol.for 方法可以检测上下文中是否已经存在使用该方法且相同参数创建的 symbol 值,如果存在则返回已经存在的值,如果不存在则新建。
const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1 === s2); // true
Symbol.keyFor 方法返回一个使用 Symbol.for 方法创建的 symbol 值的 key
const foo = Symbol.for("foo");
const key = Symbol.keyFor(foo);
console.log(key) // "foo"
使用场景
上面简单介绍一下几个常用的方法,那么在什么样的场景下能够使用到呢,接下来介绍几个比较适合的使用场景
1. 消除魔法字符
代码千万行,维护第一难。编码不规范,同事两行泪。
当代码中充斥着大量的魔法字符时,纵使是原开发者在经过一段时间后再回头看也会变得难以理解,更不必说是交由后来开发者维护。
假如现有一个 Tabs 切换的功能
if (type === 'basic') {
return <div>basic tab</div>
}
if (type === 'super') {
return <div>super tab</div>
}
上面代码中字符串 basic、super 就是与业务代码无关的魔法字符,接下来使用 Symbol 对这块代码进行改造
const tabTypes = {
basic: Symbol(),
super: Symbol(),
}
if (type === tabTypes.basic) {
return <div>basic tab</div>
}
if (type === tabTypes.super) {
return <div>super tab</div>
}
2、作为对象属性 当一个复杂对象中含有多个属性的时候,很容易将某个属性名覆盖掉,利用 Symbol 值作为属性名可以很好的避免这一现象。
const name = Symbol('name');
const obj = {
[name]: 'ClickPaas',
}
3、模拟类的私有方法
ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟。
const speak = Symbol();
class Person {
[speak]() {
...
}
}
因为使用者无法在外部创建出一个相同的 speak,所以就无法调用该方法
Symbol
(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为Object API提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
01.符号的基本用法
符号需要使用Symbol()
函数初始化。因为符号本身是原始类型,所以typeof
操作符对符号返回symbol
。
let sym = Symbol();
console.log(typeof sym); // symbol
调用Symbol()
函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建Symbol()
实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);
02.使用全局符号注册表
Symbol.for()
对每个字符串键都执行幂等操作。
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()
的任何值都会被转换为字符串。
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
此外,注册表中使用的键同时也会被用作符号描述。
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
Symbol.keyFor():来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined
。如果传给Symbol.keyFor()
的不是符号,则该方法抛出TypeError
:
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
//传给`Symbol.keyFor()`的不是符号
Symbol.keyFor(123); // TypeError: 123 is not a symbol
03.使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()
/Object.defineProperties()
定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {value: 'baz val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}
类似于Object.getOwnPropertyNames()
返回对象实例的常规属性数组,Object.getOwnPropertySymbols()
返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()
会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()
会返回两种类型的键:
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol);
// Symbol(bar)
04.常用内置符号
ECMAScript 6也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol
工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of
循环会在相关对象上使用Symbol.iterator
属性,那么就可以通过在自定义对象上重新定义Symbol.iterator
的值,来改变for-of
在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数Symbol
的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意 在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为
@@
。比如,@@iterator
指的就是Symbol.iterator
。
05.Symbol.asyncIterator
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator
。由for-await-of
语句使用”。换句话说,这个符号表示实现异步迭代器API的函数
for-await-of
循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator
为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator
:
class Foo {
async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
技术上,这个由Symbol.asyncIterator
函数生成的对象应该通过其next()
方法陆续返回Promise
实例。可以通过显式地调用next()
方法返回,也可以隐式地通过异步生成器函数返回:
class Emitter {
constructor(max) {
this.max = max;
this.asyncIdx = 0;
}
async *[Symbol.asyncIterator]() {
while(this.asyncIdx < this.max) {
yield new Promise((resolve) => resolve(this.asyncIdx++));
}
}
}
async function asyncCount() {
let emitter = new Emitter(5);
for await(const x of emitter) {
console.log(x);
}
}
asyncCount();
// 0
// 1
// 2
// 3
// 4
注意 Symbol.asyncIterator
是ES2018规范定义的,因此只有版本非常新的浏览器支持它。关于异步迭代和for-await-of
循环的细节,参见附录A。
06.Symbol.hasInstance
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof
操作符使用”。instanceof
操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof
的典型使用场景如下:
function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true复制代码
在ES6中,instanceof
操作符会使用Symbol.hasInstance
函数来确定关系。以Symbol.hasInstance
为键的函数会执行同样的操作,只是操作数对调了一下:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true复制代码
这个属性定义在Function
的原型上,因此默认在所有函数和类上都可以调用。由于instanceof
操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:
class Bar {}
class Baz extends Bar {
static [Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false复制代码
07.Symbol.isConcatSpreadable
根据ECMAScript规范,这个符号作为一个属性表示“一个布尔值,如果是true
,则意味着对象应该用Array.prototype.concat()
打平其数组元素”。ES6中的Array.prototype.concat()
方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable
的值可以修改这个行为。
-
数组对象默认情况下会被打平到已有的数组,
false
或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true
或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在Symbol.isConcatSpreadable
被设置为true
的情况下将被忽略。let initial = ['foo']; let array = ['bar']; console.log(array[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(array)); // ['foo', 'bar'] array[Symbol.isConcatSpreadable] = false; console.log(initial.concat(array)); // ['foo', Array(1)] let arrayLikeObject = { length: 1, 0: 'baz' }; console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(arrayLikeObject)); // ['foo', {...}] arrayLikeObject[Symbol.isConcatSpreadable] = true; console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz'] let otherObject = new Set().add('qux'); console.log(otherObject[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(otherObject)); // ['foo', Set(1)] otherObject[Symbol.isConcatSpreadable] = true; console.log(initial.concat(otherObject)); // ['foo']复制代码
8.Symbol.iterator
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of
语句使用”。换句话说,这个符号表示实现迭代器API的函数。
for-of
循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以Symbol.iterator
为键的函数,并默认这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的Generator
:
class Foo {
*[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]());
// Generator {<suspended>}复制代码
技术上,这个由Symbol.iterator
函数生成的对象应该通过其next()
方法陆续返回值。可以通过显式地调用next()
方法返回,也可以隐式地通过生成器函数返回:
class Emitter {
constructor(max) {
this.max = max;
this.idx = 0;
}
*[Symbol.iterator]() {
while(this.idx < this.max) {
yield this.idx++;
}
}
}
function count() {
let emitter = new Emitter(5);
for (const x of emitter) {
console.log(x);
}
}
count();
// 0
// 1
// 2
// 3
// 4复制代码
注意 迭代器的相关内容将在第7章详细介绍。
9.Symbol.match
-
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由
String.prototype.match()
方法使用”。String.prototype.match()
方法会使用以Symbol.match
为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String
方法的有效参数:console.log(RegExp.prototype[Symbol.match]); // f [Symbol.match]() { [native code] } console.log('foobar'.match(/bar/)); // ["bar", index: 3, input: "foobar", groups: undefined]复制代码
给这个方法传入非正则表达式值会导致该值被转换为
RegExp
对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义Symbol.match
函数以取代默认对正则表达式求值的行为,从而让match()
方法使用非正则表达式实例。Symbol.match
函数接收一个参数,就是调用match()
方法的字符串实例。返回的值没有限制:class FooMatcher { static [Symbol.match](target) { return target.includes('foo'); } } console.log('foobar'.match(FooMatcher)); // true console.log('barbaz'.match(FooMatcher)); // false class StringMatcher { constructor(str) { this.str = str; } [Symbol.match](target) { return target.includes(this.str); } } console.log('foobar'.match(new StringMatcher('foo'))); // true console.log('barbaz'.match(new StringMatcher('qux'))); // false复制代码
10.Symbol.replace
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()
方法使用”。String.prototype.replace()
方法会使用以Symbol.replace
为键的函数来对正则表达式求值。
11.Symbol.search
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()
方法使用”。String.prototype.search()
方法会使用以Symbol.search
为键的函数来对正则表达式求值。
12.Symbol.species
根据ECMAScript规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。
13.Symbol.split
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()
方法使用”。String.prototype.split()
方法会使用以Symbol.split
为键的函数来对正则表达式求值。
14.Symbol.toPrimitive
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive
抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive
属性上定义一个函数可以改变默认行为。
15.Symbol.toStringTag
根据ECMAScript规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()
使用”。
通过toString()
方法获取对象标识时,会检索由Symbol.toStringTag
指定的实例标识符,默认为"Object"
。内置类型已经指定了这个值,但自定义类实例还需要明确定
16.Symbol.unscopables
根据ECMAScript规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with
环境绑定中排除”。设置这个符号并让其映射对应属性的键值为true
,就可以阻止该属性出现在with
环境绑定中
3.2.5 Object类型
对象通过new
操作符后跟对象类型的名称来创建。
let o = new Object();复制代码
这个语法类似Java,但ECMAScript只要求在给构造函数提供参数时使用括号。如果没有参数,如上面的例子所示,那么完全可以省略括号(不推荐):
let o = new Object; // 合法,但不推荐复制代码
Object
的实例本身并不是很有用,但理解与它相关的概念非常重要。类似Java中的java.lang.Object
,ECMAScript中的Object
也是派生其他对象的基类。Object
类型的所有属性和方法在派生的对象上同样存在。
每个Object
实例都有如下属性和方法。
constructor
:用于创建当前对象的函数。在前面的例子中,这个属性的值就是Object()
函数。hasOwnProperty(*propertyName*)
:用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty("name")
)或符号。isPrototypeOf(*object*)
:用于判断当前对象是否为另一个对象的原型。(第8章将详细介绍原型。)propertyIsEnumerable(*propertyName*)
:用于判断给定的属性是否可以使用(本章稍后讨论的)for-in
语句枚举。与hasOwnProperty()
一样,属性名必须是字符串。toLocaleString()
:返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。toString()
:返回对象的字符串表示。valueOf()
:返回对象对应的字符串、数值或布尔值表示。通常与toString()
的返回值相同。
因为在ECMAScript中Object
是所有对象的基类,所以任何对象都有这些属性和方法。第8章将介绍对象间的继承机制。
注意 严格来讲,ECMA-262中对象的行为不一定适合JavaScript中的其他对象。比如浏览器环境中的BOM和DOM对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受ECMA-262约束,所以它们可能会也可能不会继承
Object
。
掘金:https://juejin.im/user/1398234520230989/posts;公众号:小圆脸儿; "小圆脸儿;公众号:xiaoyuanlianer666