JS:关于JS字面量及其容易忽略的12个小问题

时间:2023-03-09 01:04:47
JS:关于JS字面量及其容易忽略的12个小问题

简要

问题1:不能使用typeof判断一个null对象的数据类型

问题2:用双等号判断两个一样的变量,可能返回false

问题3:对于非十进制,如果超出了数值范围,则会报错

问题4:JS浮点数并不精确,0.1+0.2 != 0.3

问题5:使用反斜杠定义的字符串并不换行,使用反引号才可以

问题6:字符串字面量对象都是临时对象,无法保持记忆

问题7:将字符转义防止页面注入攻击

问题8:使用模板标签过滤敏感字词

问题9:格式相同,但不是同一个正则对象

问题10:非法标识符也可以用用对象属性,但只能被数组访问符访问

问题11:数组字面量尾部逗号会忽略,但中间的不会

问题12:函数表达式也可以有函数名称


JS这种语言一不小心就会写错。为什么前端技术专家工资那么高,可能要解决的疑难杂症最多吧。

什么是字面量?

在JS中,以特定符号或格式规定的,创建指定类型变量的,不能被修改的便捷表达式。因为是表达式,字面量都有返回值。字面量是方便程序员以简单的句式,创建对象或变量的语法糖,但有时候以字面量创建的“对象”和以标准方法创建的对象的行为并不完全相同。

null 字面量

举个票子,最简单的空值字面量。例如:

var obj = null

问题1:不能使用typeof判断一个null对象的数据类型

null 就是一个字面量,它创建并返回Null类型的唯一值null,代表对象为空。null是Null类型,但如果以关键字typeof关键字检测之,如下所示:

typeof null // object

返回却是object类型。这是一个历史遗留Bug,在写JS代码时,不可以用这样的方式判断null的对象类型:

if (typeof 变量 == "object") { 
console.log("此时变量一定是object类型?错!")
}

问题2:用双等号判断两个一样的变量,可能返回false

在JS*有种七种基本数据类型:Undefined、Null、布尔值、字符串、数值、对象、Symbol。其中Null、Undefined是两个特殊的类型。这两个基本类型,均是只有一个值。

null做为Null类型的唯一值,是一个字面量;undefined作为Undefined类型的唯一值,却不是字面量。undefined与NaN、Infinity(无穷大)都是JS全局定义的只读变量,它们都可以被二次赋值:

undefined = 123
NaN = 123
Infinity = 123
null = 123 // 报错:Uncaught Reference Error

NaN 即 Not a Number ,不是一个数字。NaN是唯一一个不等于自身的JS常量:

console.log(NaN == NaN) //false
var a = NaN, b = NaN 
console.log(a == b) //false

在上面代码中,用双等号判断两个变量a、b是否相等,结果返回false。仍然理论上它们是一样的。

isNaN() 用于检查一个值是否能被 Number() 成功转换,能转换返回true,不能返回false 。但并不能检测是不是纯数字,例如:

isNaN('123ab') // true 不能转换
isNaN('123.45abc')// true 不能转换

整数字面量

JS整数共有四种字面量格式:十进制、二进制、八进制、十六进制。

问题3:对于非十进制,如果超出了数值范围,则会报错

八进制

八进制字面值的第一位必须是0,然后是八进制数字序列(0-7)。如果字面值中的数值超出了范围,那么前导0将被忽略,后面的数值被当作十进制数解析。例如:

var n8 = 012
console.log(n8) //10
var n8 = 09
console.log(n8) //9,超出范围了

在es5之前,使用Number()转化八进制,会按照十进制数字处理,现在可以了。如下所示:

Number(010) //输出8

十六进制

十六进制字面值的前两位必须是0x,后跟十六进制数字序列(0-9,a-f),字母可大写可小写。如果十六进制中字面值中的数值超出范围则会报错。

var n16 = 0x11
console.log(n16) //17
var n17 = 0xw
console.log(n17) //报错

二进制

二进制字面值的前两位必须是0b,如果出现除0、1以外的数字会报错。

var n2 = 0b101
console.log(n2) //5
var n3 = 0b3
console.log(n3) //报错

浮点字面量

在JS中,所有数值都是使用64位浮点类型存储。

问题4:JS浮点数并不精确,0.1+0.2 != 0.3

由于JS采用了IEEE754格式,浮点数并不精确。例如:

console.log(0.1 + 0.2 === 0.3) // false
console.log(0.3 / 0.1) // 不是3,而是2.9999999999999996
console.log((0.3 - 0.2) === (0.2 - 0.1)) // false

因为浮点数不精确,所以软件中关于钱的金额都是用分表示,而不是用元。那为什么会不精准?

人类写的十进制小数,在计算机世界会转化为二进制小数。例如10.111这个二进制小数,换算为十进制小数是2.875,如下:

1*2^1 + 0 + 1*2^-1 + 1*2^-2 + 1*2^-3 = 2+1/2+1/4 +1/8= 2.875

对于上面提到的0.3这个十进制小数,换算成二进制应该是什么?

0.01 = 1/4 = 0.25 //小
0.011 =1/4 + 1/8 = 0.375 //又大了
0.0101 = 1/4 + 1/16 = 0.334 //还大
0.01001 = 1/4 + 1/32 = 0.28125 //又小了
0.010011 = 1/4+ 1/32 + 1/64 = 0.296875 //接近了

小数点后面每一位bit代表的数额不同,攒在一起组成的总数额也不是均匀分布的。只能无限的接近,并不能确准的表达。准确度是浮动的,所以称为浮点数。但这种浮动也不是无限的。

根据国际标准IEEE754,JS的64浮点数的二进制位是这样组成的:

1: 符号位,0正数,1负数
11: 指数位,用来确定范围
52: 尾数位,用来确定精度

后面的有效数字部分,最多有52个bit。这52个bit用完了,如果仍未准确,也只能这样了。在做小数比较时,比较的是最后面52位bit,它们相等才是相等。所以,0.1 + 0.2不等于0.3也不稀奇了,在数学上它们相等,在计算机它们不等。

但这种不精确并不是JS的错,所有编程语言的浮点数都面临同样问题。

字符串字面量

字符串字面量是由双引号(")对或单引号(')括起来的零个或多个字符。格式符必须是成对单引号或成对双引号。例如:

"foo"
'bar'

问题5:使用反斜杠定义的字符串并不换行,使用反引号才可以

使用反斜杠可以书写多行字符串字面量:

var str = "this string \
is broken \
across multiple\
lines."

但是这种多行字符串在输出并不是多行的:

console.log(str) //输出"this string is broken across multiplelines."

如果想实现Here文档(注1)的字符串效果,可以使用转义换行符:

var poem = 
"Roses are red,\n\
Violets are blue.\n\
Sugar is sweet,\n\
and so is foo."

在es6里面,定义了模板字符串字面量,使用它创造多行字符串更简单:

var poem = `Roses are red,
Violets are blue.
Sugar is sweet,
and so is foo.`

问题6:字符串字面量对象都是临时对象,无法保持记忆

在字符串字面值返回的变量上,可以使用字符串对象的所有方法。例如调用length属性:

console.log("Hello".length)

但是字面量字符串返回的对象,并不完全等于字符串对象。前者与String()创建的对象有本质不同,它无法创建并保持属性:

var a = "123"
a.abc = 100
console.log(a.abc) //输出undefined

a = new String("123")
a.abc = 100
console.log(a.abc) //输出100

可以认为,使用字符串字面量创建的对象均是临时对象,当调用字符串字符量变量的方法或属性时,均是将其内容传给String()重新创建了一个新对象,所以调用方法可以,调用类似于方法的属性(例如length)也可以,但是使用动态属性不可以,因为在内存堆里已经不是同一个对象了。

想象这个场景可能是这样的:

程序员通过字面量创建了一个字符串对象,并把一个包裹交给了他,说:“拿好了,一会交给我”。字符串对象进CPU车间跑了一圈出来了,程序员一看包裹丢了,问:“刚才给你的包裹哪里了?”。字符串对象纳闷:“你什么时候给我包裹了?我是第一次见到你”

特殊符号

使用字符串避不开特殊符号,最常用的特殊符号有换行(\n),制表符(\t)等。

在这里反斜杠(\)是转义符号,代表后面的字符具有特殊含义。双此号(")、单引号(')还有反引号(`),它们是定义字符串的特殊符号,如果想到字符串使用它们的本意,必须使用反斜杠转义符。例如:

console.log("双引号\" ,反斜杠\\,单引号\'")
//双引号" ,反斜杠\,单引号'

这里是一份常规的转义符说明:

一个特殊符号有多种表示方式,例如版本符号,这三种方式都可以:

console.log("\251 \xA9 \u00A9") //输出"© © ©"

这是一份常用转义符号使用16进制表示的Unicode字符表:

像上面的示例:

console.log("双引号\" ,反斜杠\\,单引号\'")

也可以这样写:

console.log("双引号\u0022 ,反斜杠\u005C,单引号\u0027")
//输出"双引号" ,反斜杠\,单引号'"

论装逼指数,这种谁也看不明白的Unicode码,比直观的转义序列码难度系数更高。

问题7:将字符转义防止页面注入攻击

含有Html标签符号的字符串,在数据存储或页面展示时,有时候需要将它们转义;有时候又需要将它们反转义,以便适合人类阅读:

function unescapeHtml(str) { 
var arrEntities={'lt':'<','gt':'>','nbsp':' ','amp':'&','quot':'"'}; 
return str.replace(/&(lt|gt|nbsp|amp|quot);/ig,function(all,t){return arrEntities[t];}); 
}

function htmlEscape(text){ 
return text.replace(/[<>"&]/g, function(match, pos, originalText){
switch(match){
case "<": return "<"; 
case ">":return ">";
case "&":return "&"; 
case "\"":return """; 

}); 
}
htmlEscape("<hello world>") // "<hello world>"
unescapeHtml("<hello world>") // "<hello world>"

模板字符串字面量

在es6中,提供了一种模板字符串,使用反引号(`)定义,这也是一种字符串字面量。这与Swift、Python等其他语言中的字符串插值特性非常相似。例如:

let message = `Hello world` //使用模板字符串字面量创建了一个字符串

使用模板字符串,原来需要转义的特殊字符例如单引号、双引号,都不需要转义了:

console.log(`双引号" ,单引号'`)//双引号" ,单引号'

使用模板字面量声明多行字符串,前面已经讲过了。需要补充的是,反引号中的所有空格和缩进都是有效字符 。

模板字符串最方便的地方,是可以使用变量置换,避免使用加号(+)拼接字符串。例如:

var name = "李宁"
var msg = `欢迎你${name}同学`
console.log(msg)//欢迎你李宁同学

问题8:使用模板标签过滤敏感字词

模板字面量真正的强大之处,不是变量置换,而是模板标签。模板标签像模板引擎的过滤函数一样,可以将原串与插值在函数中一同处理,将将处理结果返回。这可以在运行时防止注入攻击和替换一些非法违规字符。

这是一个模板标签的使用示例:

let name = '李宁', age = 20
let message = show`我来给大家介绍${name},年龄是${age}.`;
function show(arr, ...args) {
console.log(arr) // ["我来给大家介绍", ",年龄是", ".", raw: Array(3)]
console.log(args[0]) // 张三
console.log(args[1]) // 20
return "隐私数据拒绝展示"
}
console.log(message) //隐私数据拒绝展示

变量message的右值部分是一个字符串模板字面量,show是字面量中的模板标签,同时也是下方声明的函数名称。模板标签函数的参数,第一个是一个被插值分割的字符串数组,后面依次是插值变量。在模板标签函数中,可以有针对性对插值做一些技术处理,特别当这些值来源于用户输入时。

正则表达式字面量

JS正则表达式除了使用new RegExp()声明,使用字面量声明更简洁。定义正则表达式字面量的符号是正斜杠(/)。例如:

var re = /[a-z]/gi
console.log("abc123XYZ".replace(re, "")) // 123

re即是一个正则表达式,它将普通字符串转换为数值字符串。正斜杠后面的g与i是模式修饰符。常用的模式修饰符有:

g 全局匹配
m 多行匹配
i 忽略大小写匹配

模式修饰符可以以任何顺序或组合出现,无先后之分。上面的正则表达式,使用标准形式创建是这样的:

var re = new RegExp("[a-z]","gi")
console.log("abc123XYZ".replace(re, "")) // 123

显然,使用字面量声明正则更简单。

正则表达式字面量不能为空,如果为空将开始一个单行注释。如果要指定一个空正则,使用/(?:)/。

问题9:格式相同,但不是同一个正则对象

在es5之前,使用字面量创建的正则,如果正则规则相同,则它们是同一个对象:

function getReg() {
var re = /[a-z]/
re.foo = "bar"
return re
}
var reg1 = getReg()
var reg2 = getReg()
console.log(reg1 === reg2) // true
reg2.foo = "baz"
console.log(reg1.foo) // "baz"

从上面代码中,可以看出reg1与reg2是值与类型全等。改变reg2的属性foo,reg1的foo属性同步改变。它们是内存堆中是一个对象。这种Bug在es5中已经得到修正。

对象字面量

重点来了,这是被有些人称为神乎其技的对象字面量。

JS的字面量对象,是一种简化的创建对象的方式,和用构造函数创建对象一样存在于堆内存当中。对象字面值是封闭在花括号对({})中的一个对象的零个或多个"属性名-值"对的元素列表。不能在一条语句的开头就使用对象字面值,这将导致错误或产生超出预料的行为, 因为此时左花括号({)会被认为是一个语句块的起始符号。

这是是一个对象字面值的例子:

var car = { 
name: "sala", 
getCar: function(){}, 
special: "toyota"
}

对象字面值可以嵌套,可以在一个字面值内嵌套上另一个字面值,可以使用数字或字符串字面值作为属性的名字。例如:

var car = { other: {a: "san", "b": "jep"} }

问题10:非法标识符也可以用用对象属性,但只能被数组访问符访问

数字本身是不能作为标识符的,但在对象字面中却可以作为属性名。在访问这样的“非法”属性时,不能使用传统的点访问符,需要使用数组访问符:

var foo = {a: "alpha", 2: "two"}
console.log(foo.a) // alpha
console.log(foo[2]) // two
console.log(foo.2) // 错误

除了数字之外,其它非法标识符例如空格、感叹号甚至空字符串,都可以用于属性名称中。当然访问这些属性仍然离不了数组访问符:

var s = {
"": "empty name",
"!": "bingo"
}
console.log(s."") // 语法错误
console.log(s[""]) // empty name
console.log(unusualPropertyNames["!"])

增强性字面量支持

在es6中,对象字面量的属性名可以简写、方法名可以简写、属性名可以计算。例如:

var name = "nana", age = 20, weight = 78
var obj = {
name, // 等同于 name: nana
age, // 等同于 age: 20
weight, // 等同于 weight: 78

sayName() { // 方法名简写,可以省略 function 关键字
console.log(this.name);
},

// 属性名是可计算的,等同于over78
['over' + weight]: true
}
console.log(ogj) // {name: "nana", age: 20, weight: 78, over78: true, descripte: ƒ}

注意每个对象元素之间,需要以逗号分隔,每个元素没有字面上的键名,但其实也是一个键值对。甚至在创建字面量对象时,可以使用隐藏属性__proto__设置原型,并且支持使用super调用父类方法:

var superObj = {
name: "nana",
toString(){
return this.name 
}
}
var obj = {
__proto__: superObj,
toString() {
return "obj->super:" + super.toString();
}
}
console.log(obj.toString()) // obj->super:nana

属性赋值器(setter)和取值器(getter),也是采用了属性名简写:

var cart = {
wheels: 4,
get wheels () {
return this.wheels
},
set wheels (value) {
if (value < this.wheels) {
throw new Error(' 数值太小了! ')
}
this.wheels = value;
}
}

因为有增加性的属性名、方法名简写,当在CommonJS 模块定义中输出对象时,可以使用简洁写法:

module.exports = { getItem, setItem, clear }
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
}

数组字面量

数组字面量语法非常简单,就是逗号分隔的元素集合,并且整个集合被方括号包围。例如:

var coffees = ["French", 123, true,]
console.log(a.length) // 1

等号右值即是一个数组字面量。使用Array()构造方法创建数组,第一个参数是数组长度,而不是数组元素:

var a = new Array(3)

console.log(a.length) // 3

console.log(typeof a[0]) // "undefined"

问题11:数组字面量尾部逗号会忽略,但中间的不会

尾部逗号在早期版本的浏览器中会报错,现在如果在元素列表尾部添加一个逗号,它将被忽略。但是如果在中间添加逗号:

var myList = ['home', , 'school', ,]

却不会被忽略。上面这个数组有4个元素,list[1]与list[3]均是undefined。

函数字面量

函数是JS编程世界的一等公民。JS定义函数有两种方法,函数声明与函数表达式,后者又称函数字面量。平常所说的匿名函数均指采用函数字面量形式的匿名函数。

(一)这是使用关键字(function)声明函数:

function fn(x){ alert(x) }

(二)这是函数字面量:

var fn = function(x){ alert(x) }

普通函数字面量由4部分组成:

  • 关键词 function
  • 函数名,可有可无
  • 包含在括号内的参数,参数也是可有可无的,括号却不能少
  • 包裹在大括号内的语句块,即函数要执行的具体代码

(三)这是使用构造函数Function()创建函数:

var fn= new Function( 'x', 'alert(x)' )

最后一种方式不但使用不方便,性能也堪忧,所以很少有人提及。

问题12:函数表达式也可以有函数名称

函数字面量仍然可以有函数名,这方面递归调用:

var f = function fact(x) {
if (x < = 1) {
return 1
} else {
return x*fact(x-1)
}
}

箭头函数

在es6中出现了一种新的方便书写的匿名函数,箭头函数。例如:

x => x * x

没有function关键字,没有花括号。它延续lisp语言lambda表达式的演算风格,不求最简只求更简。箭头函数没有名称,可以使用表达式赋值给变量:

var fn = x => x * x

作者认为它仍然是一种函数字面量,虽然很少有人这样称呼它。

布尔字面量

布尔字面量只有true、false两个值。例如:

var result = false // false字面量


注1:here文档,又称作heredoc,是一种在命令行shell和程序语言里定义字符串的方法。

参考资料

【1】《javascript权威指南(第6版)》

【2】《javascript高级程序设计(第3版)》

【3】《javascript语言精粹(修订版)》

【4】《javascript DOM编程艺术(第2版)》

【5】《javascript启示录》

首先于微信公众号“艺述思维”:关于JS字面量及其容易忽略的12个小问题