专栏首页艺述思维关于JS字面量及其容易忽略的12个小问题

关于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("双引号\" ,反斜杠\\,单引号\'") //双引号" ,反斜杠\,单引号'

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

字符

意思

\0

Null字节

\b

退格符

\f

换页符

\n

换行符

\r

回车符

\t

Tab (制表符)

\v

垂直制表符

\'

单引号

\"

双引号

\\

反斜杠字符(\)

\XXX

由从0到377最多三位八进制数XXX表示的 Latin-1 字符。例如\251是版权符号的八进制序列。

\xXX

由从00和FF的两位十六进制数字XX表示的Latin-1字符。例如\xA9是版权符号的十六进制序列。

\uXXXX

由四位十六进制数字XXXX表示的Unicode字符。例如\u00A9是版权符号的Unicode序列。

\u{XXXXX}

Unicode代码点 (code point) 转义字符。例如\u{2F804} 相当于Unicode转义字符 \uD87E\uDC04的简写。

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

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

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

转义序列

字符编码值

名称

\b

\u0008

回格

\t

\u0009

水平制表符

\n

\u000A

进行(新行)

\v

\u000B

竖直制表符

\f

\u000C

进纸

\r

\u000D

回车

\"

\u0022

双引号

\'

\u0027

单引号

\\

\u005C

反斜杠

像上面的示例:

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启示录》

本文分享自微信公众号 - 用故事讲技术(ygsjjs),作者:石桥码农

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 关于前端开发中的模块化

    AngularJS模块化使用的并不是标准的AMD规范,AngularJS的风格风格大致是这样的:

    李艺
  • 微信小程序实践:2.3 可滚动的容器组件之 scroll-view

    说什么真理无穷,进一寸有一寸的欢喜。大家好,我是石桥码农,今天继续为大家分享微信小程序实践相关的技术内容。

    李艺
  • vue 开发常用工具及配置二

    作者在个人工作中喜欢用go语言,因为它简单方便。先安装go语言环境,然后于终端执行如下命令:

    李艺
  • 数据类型的转换

    天天_哥
  • 【前端芝士树】浅拷贝、深拷贝以及Object.assign()的作用、克隆对象、复制数组

    基本类型值是指在栈内存保存的简单数据段,在复制基本类型值的时候,会开辟出一个新的内存空间,将值复制到新的内存空间,举个栗子:

    CloudCat
  • 面试官想要的 JS 基本类型

    作者曾经也是这样回答的,并且一直觉得没有什么问题。那么面试官问你 JS 数据类型时,他想知道什么呢?接着往下看???

    grain先森
  • 举例说明了十大ES6功能

    虽然ES6规范不是最近才发布,但我认为很多开发人员仍然不太熟悉。 主要原因是在规范发布之后,Web浏览器的支持可能很差。 目前,规范发布已经超过2年了,现在很多...

    frontoldman
  • javascript第6讲:赋值运算,一元运算,位运算

    当第一个是字符,第二个是数字时,会根据第一个的类型进行运算 使用赋值运算符运算时遵照算数运算完成

    小海怪的互联网
  • 第七章 正则表达式编程

    第七章 正则表达式编程 什么叫知识,能指导我们实践的东西才叫知识。 学习一样东西,如果不能使用,最多只能算作纸上谈兵。正则表达式的学习,也不例外。 掌握了正则表...

    程序猿DD
  • [第13期] 掌握前端面试基础系列一: ES6

    这里我们可以看到, 第一行中的a虽然还没声明, 但是我们用起来却不会报错。这种情况, 就是变量声明的提升。

    用户6900878

扫码关注云+社区

领取腾讯云代金券