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