JS:關於JS字面量及其容易忽略的12個小問題

石橋碼農發表於2019-01-11

簡要

 

問題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個小問題

 

相關文章