javascript
《JavaScript高级程序设计(第四版)》红宝书学习笔记(1)
個人對第四版紅寶書的學習筆記。不適合小白閱讀。這是part1,包含原書第二章(HTML中的Javascript)和第三章(語言基礎)。持續更新,其他章節筆記看我主頁。
(記 * 的表示是ES6新增的知識點,記 · 表示包含新知識點)。
新增知識點如下:let聲明、const聲明、模板字面量(字符串)、Symbol數據類型、for-of 循環語句。
第二章:HTML中的Javascript
2.1 <script>元素
<script>標簽的八個屬性
-
async:可選。表示應該立即開始下載腳本,但不能阻止其它頁面動作,比如下載資源或等待其他腳本加載。使用該屬性可以異步執行腳本。只對外部腳本文件有效。
-
charset:可選。使用src屬性指定的代碼字符集。基本不會使用。
-
crossorigin:可選。配置相關的CORS(跨資源共享)設置。默認不使用CORS。corssorigin="anonymous"配置文件請求不必設置憑據標志。corssorigin="use-credentials"設置憑據標志,意味著出戰請求會包含憑據。
-
defer:可選。表示在文檔解析和顯示完成后再執行腳本是沒有問題的。只對外部腳本文件有效。IE7及以前可以對行內腳本指定該屬性。使用該屬性推遲執行腳本。
-
integrity:可選。允許對比接收到的資源和指定的加密簽名以驗證子資源完整性(SRI)。如果接收到資源的簽名與這個屬性指定的簽名不匹配,則頁面會報錯,腳本不會執行。該屬性可以用于確保內容分發網絡(CDN,Content Delivery Network)不會提供惡意內容。
-
language:已廢棄。
-
src:可選。表示包含要執行的代碼的外部文件。
-
type:可選。代替language,表示代碼塊中腳本語言的內容類型(也稱MIME類型)。按慣例始終都會是text/javascript。Javascript文件的MIME類型通常是"application/x-javascript",不過給type屬性這個值有可能導致腳本被忽略。在非IE瀏覽器有效的其他值還有"application/javascript"和"application/ecmascript"。如果這個值是module,則代碼會被當成ES6模塊,且只有這時候代碼中才能出現import和export關鍵字。
三個注意點:
使用<script>標簽寫行內代碼時不要出現這種情況:
<script>console.log("</script>"); <!--這里</script>會被當作結束標簽,甚至你會發現寫在注釋中也會被當作結束標簽無法解析,因此我這里用了html的注釋法--> </script>可以使用轉義字符解決這一問題:
<script>console.log("<\/script>"); //這里可以正常使用,注釋里這樣使用:<\/script> 也可以,從代碼沒有高亮可以看出來 </script>該元素最為強大同時也備受爭議的特性是:它可以包含來自外部域的Javascript文件。它的src屬性可以是一個完整的url,而且這個url指向的資源可以跟包含它的HTML頁面不再同一個域中。例如:
<script src="http://www.somewhere.com/afile.js"></script>
瀏覽器解析時會向src屬性指定的路徑發送一個GET請求,以取得相應資源。這個初始的請求不受瀏覽器同源策略限制,但返回的Javascript受限。(當然,該請求仍然受HTTP/HTTPS)協議的限制。
來自外部的代碼會被當成加載他它的頁面的一部分來加載和解析。**這個能力可以讓我們通過不同的域分發JavaScript。**不過同時,引用他人服務器的文件時必須格外小心,因為可能會有惡意的程序員替換這個文件。integrity屬性可以防范,但不是所有瀏覽器都支持。
一般瀏覽器會按照<script>在頁面中的順序依次解釋它們,只要沒有使用defer和async屬性的話。另外最好將標簽位置放在頁面底部(<body>之后)。
###2.2 行內代碼與外部文件
雖然不是明確的強制性規則,但通常認為最佳實踐是盡可能將Javascript代碼放在外部文件中。原因如下:
- 可維護性。使用一個目錄保存所有的JavaScript文件總會比分散在很多HTML頁面容易維護。
- 緩存。瀏覽器會根據特點的設置緩存所有外部鏈接的JavaScript文件,這意味著如果兩個頁面都用到同一個文件,則該文件只需被下載依次。
- 適應未來。
2.3 文檔模式
IE5.5發明了文檔模式的概念,即可以通過doctype切換文檔模式。最初的文檔模式有兩種:混雜模式(quirks mode)和標準模式(standards mode)。后來出現了第三種文檔模式:準文檔模式(almost standards mode)。只作了解。
2.4 <noscript>元素
針對早期瀏覽器不支持JavaScript的問題,提出的一個頁面優雅降級的處理方案:
<noscript><p>該頁面不支持JavaScript,請更換瀏覽器。</p> </noscript><noscript>元素可以包含任何出現在<body>中的HTML元素,<script>除外。出現下列兩種情況下,瀏覽器將顯示包含在該元素中的內容:
- 瀏覽器不支持腳本;
- 瀏覽器對腳本的支持被關閉。
第三章:語言基礎
3.1 變量聲明
3.1.1 var聲明
var聲明的變量不初始化的情況下,該變量會保存特殊的值undefined。
var聲明廚初始化后的變量,后續可以改變類型。
1)var聲明作用域
使用var操作符定義的變量會成為包含它的函數的局部變量,該變量在函數退出時被銷毀。而在局部作用域中省略var操作符聲明,該變量會作為全局變量:
function test(){var m1="hi";m2="hi"; } test(); console.log(m1); //-> 出錯! console.log(m2); //-> hi2)var聲明提升
使用var關鍵字時,聲明的變量會自動提升(hoist)到函數作用域頂部。此外,反復多次使用var聲明同一個變量也沒有問題。
function test(){console.log(age);var age=18; }/*上述代碼在ECMAScript中運行時會看成等價如下代碼:*/ function test(){var age;console.log(age);age=18; } //因此調用該方法,結果會如下: test(); //-> undefined3.1.2 let聲明 *
let和var差不多,但有著很大的區別,最明顯的區別是:let聲明的范圍是塊作用域,而var是函數作用域。
if(true){var name1='matt'; } console.log(name1); //-> matt if(true){let name2='mat'; } console.log(name2); //-> ReferenceError:name沒有定義這里name2之所以不能再if塊外部被引用,是因為它的作用域僅限于該塊內部。塊作用域是函數作用域的子集,因此適用于var的作用域限制同樣適用于let。
let也不允許同一個塊作用域中出現冗余聲明。
var age; var age;let age; let age; //-> SyntaxError; 標識符age已經聲明過了對聲明冗余報錯不會因混用let和var而受影響。它們聲明的不是不同類型的變量,只是指出變量在相關作用域如今存在。
var name; let name; //-> SyntaxErrorlet age; var age; //-> SyntaxError1)暫時性死區
let和var另一個重要的區別,就是let聲明的變量不會再作用域中被提升。
在let聲明之前的執行瞬間被稱為“暫時性死區”(temporal dead zone),在此階段引用任何后面才聲明的變量都會拋出ReferenceError。
2)全局聲明
let在全局作用域中聲明的變量不會成為window 對象的屬性(var聲明的變量則會)。不過let聲明仍然是在全局作用域中發生的,相應變量會在頁面的生命周期內存續。
var name='matt'; console.log(window.name); //-> 'matt'let age=18; console.log(window.age); //-> undefined3)條件聲明
使用var聲明時,由于聲明會被提升,JavaScript引擎會自動將多余的聲明在作用域頂部合并為一個聲明。因為let的作用域是塊,所以不可能檢查前面是否已經使用let聲明過同名變量,同時也就不可能在沒有聲明的情況下聲明它。
4)for循環中的let聲明
在let出現之前,for循環定義的迭代變量會滲透到循環體外部:
for(var i=0;i<5;i++){//循環體 } console.log(i); //-> 5而let就解決了這一問題,因為迭代遍歷的作用域僅限于for循環塊內部。
for(let i=0;i<5;i++){//循環體 } console.log(i); //-> ReferenceError:i沒有定義另外在使用var的時候,最常見的問題就是對迭代變量的奇特聲明和修改:
for(var i=0;i<5;i++){setTimeout(() => console.log(i),0); //-> 5、5、5、5、5 } //因為在退出循環時,迭代變量保存的都是導致循環退出的值:5。之后執行超時邏輯時,所有的i都是同一個變量。而在使用let聲明迭代變量時,JavaScript引擎在后臺會為每個迭代循環聲明一個新的迭代變量。
for(let i=0;i<5;i++){setTimeout(() => console.log(i),0); //-> 1、2、3、4、5 }這種行為適用于于所有風格的for循環,包括for-in和for-of循環。
3.1.3 const聲明 *
const的行為與let基本相同,唯一一個重要的區別是用它聲明變量是時必須同時初始化變量,且后續嘗試修改const變量會報錯。
const聲明的限制只適用于它指向的變量的引用。換言之,如果const變量引用的是一個對象,那么修改這個對象內部的屬性并不違反const的限制。
另外const不能用來聲明迭代變量(因為迭代變量會自增或自減)。但const可以用來聲明一個不會被修改的for循環變量,也就是說,每次迭代只是創建一個新變量。
let i=0; for(const j=7;i<5;++i){console.log(j); //-> 7,7,7,7,7 }for(const key in {a:1,b:2}){console.log(key); //-> a,b }for(const value of [1,2,3,4,5]){console.log(value); //-> 1,2,3,4,5 } //可以看出,這對for-of和for-in循環特別有意義3.2 數據類型
3.2.1 typeof操作符
使用typeof操作符檢驗數據類型。可以用來區分函數和對象。
alert(typeof 95); alert(typeof(95)); //看的出來typeof操作符也可以使用參數 alert(typeof null); //會返回object,因為特殊值null被認為是一個對空對象的引用3.2.2 Undefined類型
在聲明變量但未對其加以初始化時,這個變量的值就是undefined。
而當使用typeof操作符檢驗一個未聲明的變量時,返回的值也是undefined。
3.2.3 Null類型
邏輯上,null值表示一個空對象指針。所以使用typeof檢驗null會返回object。
ECMA-262規定 undefined==null 返回 true。
無論什么情況都不需要將變量值顯式地設置undefined,但對null不適用。換言之,只要意在保存的對象還沒有真正保存對象,就應該明確地讓該變量保存null。
3.2.4 Boolean類型
? true、false是區分大小寫的。
布爾類型轉換Boolean()
? 使用**Boolean()**函數將對應的值轉化為布爾值。下面是轉化規則:
| Boolean | true | false |
| String | 任何非空字符串 | 空字符串 |
| Number | 任何非零數字值(包括無窮值) | 0和NaN |
| Object | 任何對象 | null |
| Undefined | n/a(不適用) | undefined |
3.2.5 Numer類型
除了十進制表示以外,整數還可以通過八進制或十六進制的字面值表示。
其中,八進制字面值的第一位必須是零(0)。如果字面值中的數值超出了范圍,那么前導零將被忽略,后面的數值將被當作十進制數值解析。注意,八進制字面量在嚴格模式下是無效的,會導致支持的Js引擎拋出錯誤。
十六進制字面值的前兩位必須是0x,后跟任何十六進制數(09及AF)。其中,字母A~F可以大寫,也可以小寫。
進行算術計算時,所有的八進制和十六進制數最終都將被轉化為十進制數。
1)浮點數
由于保存浮點數值需要的內存空間是整數的兩倍,因此ECMAScript會不失時機地將浮點數值轉化為整數值。如果小數點后沒有跟任何數字(如 1.)或浮點數值本身表示的就是一個整數(如1.0),就會將其轉化為整數。
默認情況下,ECMAScript會將那些小數點后面帶有6個0以上的浮點數值轉化為以e表示法表示的數值。
浮點數值的最高精度是17位小數,但在進行算術計算時其精確度遠不如整數。例如,0.1+0.2的結果不是0.3,而是0.30000000000000004(小數點后一共17位)。這個舍入誤差會導致無法測試指定的浮點數值。例如:
if(a + b == 0.3){ //不要做這樣的測試!alert("You get 0.3"); } //在這個例子中,我們測試的是兩個數的和是不是等于0.3。若這兩個數是0.05和0.25,或者是0.15和0.15都不會有問題。因此,永遠不要測試某個特定的浮點數2)數值范圍
由于內存限制,ECMAScript無法保存世界上所有數據。ECMAScript能夠表示的最小數值保存在Number.MIN_VALUE中——在大多數瀏覽器中,這個值是5e-324;能夠表示的最大數值保存在Number.MAX_VALUE中——在大多數瀏覽器中,這個值是1.7976931348623157e+308。如果這個數值是正數,則會轉化為**Infinity**。
要想確定一個數值是不是有窮的(換言之,是不是位于最小和最大數值之間),可以使用isFinite()函數。這個函數在參數位于最小與最大數值之間會返回true。
3)NaN
? NaN,即非數值(Not a Number)是一個特殊的數值,這個數值用于表示一個本來要返回數值的操作數未返回數值的情況(這樣不會拋出錯誤)。例如在其他編程語言中,任何數除以0都會導致錯誤,但在ECMAScript中會返回NaN,因此不會影響其他代碼執行。
? NaN有兩個特點。首先,任何涉及NaN的操作都會返回NaN。其次,NaN與任何值都不相等,包括NaN本身。
? 針對NaN,ECMAScript定義了一個函數isNaN()函數。這個函數接受一個參數(可以是任意類型),函數會幫我們確定該參數是否“不是數值”。函數在接受到值后,會嘗試將該值轉化為數值,某些不是數值的值會直接轉化為數值,例如字符串"10"或Boolean值。任何不能被轉化為數值的值都會導致函數返回true。
alert(isNaN(NaN)); //true alert(isNan(10)); //false(10是一個數值) alert(isNan("10")); //false(會轉化為數值10) alert(isNan("blue")); //true(無法轉化為數值) alert(isNan(true)); //false(可以被轉化為數值1)? 而對于isNaN(),它也適用于對象。在基于對象調用該函數時,會首先調用對象的valueOf()方法,然后確定該方法返回的值是否可以轉化為數值。如果不能,則基于這個返回值再調用 toString() 方法,再測試返回值。
4)數值轉換
? 有三個函數可以將非數值轉化為數值:Number()、parseInt()和parseFloat()。第一個轉型函數Number()可以用于任何數據類型,而另兩個函數則專門用于將字符串轉換成數值。
Number()函數
-
如果是布爾值,true和false將分別轉換為1和0。
-
如果數字值,只是簡單的傳入與返回。
-
如果是null,返回0。
-
如果是undefined,返回NaN。
-
如果是字符串,遵循下列規則:
- 如果字符串中只包含數字,則將其轉化為十進制,如"12"會轉化為12,"0123"轉化為123(注意,前導的零被忽略了)。
- 如果字符串中包含有效的浮點格式,如"1.1",則將其轉化為對應的浮點數值(同樣會忽略前導零)
- 如果字符串中包含有效的十六進制格式,則將其轉化為相同大小的十進制整數;
- 如果為空字符串,轉化為0;
- 如果字符串包含上述格式之外的字符,則轉換為NaN。
-
如果是對象,則調用對象的valueOf()方法,然后依照前面的規則轉換返回的值。如果結果是NaN,則調用對象的toString()方法,然后再次依照前面的規則轉換返回的字符串值。
ParseInt()函數
由于Number()函數在轉化時比較復雜且不夠合理,因此在處理整數的時候更常用的是parseInt()函數。該函數會忽略字符串前的空格,直至找到第一個非空格字符。如果第一個字符不是數字或者負號,函數就會返回NaN;也就是說,parseInt()函數對空字符串會返回NaN(而Number()函數會返回0)。如果第一個字符是數字字符,則會繼續解析第二個字符直到全部解析完畢或者遇到了第一個非數字字符。
如果字符串中的第一個字符是數字字符,parseInt()也能識別各種整數格式。也就是說,如果字符串以"0x"開頭且后跟數字字符,就會將其當作十六進制整數,如果字符串以"0"開頭且后跟數字字符,則會將其解析為八進制數。
【注】對于八進制數如 070 ,ES3和ES5存在分歧,ES3會解析為56,而ES5會解析為0。在ES5 JS引擎中,parseInt()函數已不再具備解析八進制的能力,因此前導零無效,解析為0。嚴格模式下同樣如此。
【續】為消除可能產生的困惑,可以為這個函數提供第二個參數:轉換時使用的基數(即多少進制)。例如:
var num=parseInt("0xAF",16); //175 //而實際上如果指定了16進制,字符串可以不帶前面的0x。var num=parseInt("AF"); //NaN var num=parseInt("AF",16); //175 //指定基數會影響轉換的輸出結果 var num1=parseInt("10",2); //2 var num2=parseInt("10",8); //8為了避免解析的錯誤,建議無論在什么情況下都明確指定基數。
parseFloat()函數
該函數同parseInt()函數類似,也是從第一個字符位置開始解析每個字符,同樣解析到字符串結尾,或者解析到遇見一個無效的浮點數字符為止。也就是說,字符串中的第一個小數點是有效的,而第二個小數點就無效了。
除第一個小數點有效之外,parseFloat()函數與parseInt()的第二個區別在于它始終都會忽略前導的零。parseFloat()函數對于十六進制格式字符串則始終會轉換成0。parseFloat()沒有指定進制第二參數的用法。且若字符串包含的是一個可解析為整數的數(沒有小數點或者小數點后都為0),則會返回整數。
3.2.6 String類型
字符串的表達方式:可以使用雙引號("")、單引號(’’)和反引號(``)表示。
1)字符字面量(轉義序列)
| \n | 換行 |
| \t | 制表 |
| \b | 空格 |
| \r | 回車 |
| \f | 換頁 |
| \` \" \’ | 字符串標志符號 |
| \xnn | 以十六進制編碼nn表示的一個字符(其中n為0~F) |
| \unnnn | 以十六進制編碼nnnn表示的一個Unicode字符 |
**一個轉義序列表示一個字符。**字符串的長度可以使用length屬性獲取。
如果字符串中包含雙字節字符,那么length屬性返回的值可能不是準確的字符數。第五章會具體討論如何解決這個問題。
2)字符串的特點
ES中的字符串不可變。字符串一旦創建,它們的值就不能改變。要改變某個變量保存的字符串,首先要銷毀原來的字符串,然后再用另一個包含新值的字符串填充改變量。
3)轉換為字符串
toString()
要將一個值轉換為字符串有兩種方式。第一種是使用幾乎每個值都有的toString()方法。數值、布爾值、對象和字符串值(字符串調用該方法返回字符串的一個副本)都有該方法。但null和undefined值沒有該方法。
多數情況下,調用toString()方法不必傳遞參數。但是,在調用數值的toString()方式時,可以傳遞一個參數:輸出數值的基數。默認情況,該方法以十進制格式返回數值的字符串表示。通過傳遞基數,可以輸出其他任意有效進制格式的表示。
String()
在不知道要轉換的值是不是null或undefined的情況下,可以使用String()方法,這個函數能夠將任何類型的值轉化為字符串。該方法遵循下列轉換規則:
- 如果值有toString()方法,則調用該方法(無參數)并返回相應結果;
- 如果值為null,則返回"null";
- 如果值為undefined,則返回"undefined"。
4)模板字面量 *
ES6新增了使用模板字面量定義字符串的能力。與使用單引號和雙引號不同,模板字面量保留換行字符,可以跨行定義字符串:
let str1='first line\nsecond line'; let str2=`first line second line`; console.log(str1); /*-> first linesecond line */ console.log(str2); /*-> first linesecond line */ console.log(str1===str2); //-> true顧名思義,模板字面量在定義模板時特別有用。如下html模板:
let pageHTML=` <div><a href="#"><span>Jake</span></a> </div> `; //這里可以注意,這里字符串其實是以換行符開始的。如果打印 console.log(pageHTML[0]==='\n'); //-> 結果會是true但同時,因為模板字面量會保持反引號內部的空格,因此使用時需格外小心。(這些空格也算一個字符)
5)字符串插值 *
模板字面量最常用的一個特性是支持字符串插值,也就是可以在一個連續定義中插入一個或多個值。技術上來說,模板字面量不是字符串,而是一種特殊的Javascript句法表達式,只不過求值之后得到的是字符串。模板字面量在定義時立即求值并轉化為字符串實例,任何插入的變量也會從它們最近的作用域中取值。
使用${}實現字符串插值:
let name='Jack',age=18; let str=`My name is ${name}, I'm ${age} years old`;所有插入的值都會使用toString()強制轉型為字符串,任何JS表達式都可以用于插值(也就是說函數和方法也可以)。嵌套的模板字符串無需轉義:
console.log('Hello, ${'world'} !'); //-> Hello, world!此外,模板也可以插入自己之前的值:
let val=''; function append(){val=`${val}abc`;console.log(val); } append(); //-> abc append(); //-> abcabc append(); //-> abcabcabc6)模板字面量標簽函數 *
模板字面量也支持定義標簽函數(tag function),通過標簽函數可以自定義插值行為。標簽函數會接受被插值記號分隔后的模板和對每個表達式求值的結果。
標簽函數本身是一個常規函數,通過前綴到字面量來應用自定義行為,如下所示。標簽函數接收到的參數依次是原始字符串數組和對每個表達式求值的結果。這個函數的返回值是對模板字面量求值得到的字符串。
let a=6,b=9; function simpleTag(strings,aValExression,bValExression,sumValExpression){console.log(strings);console.log(aValExression);console.log(bValExression);console.log(sumValExpression);return 'foobar'; }let untaggedResult=`${a} + ${b} = ${a+b}`; let taggedResult=simpleTag`${a} + ${b} = ${a+b}`; // ["", " + ", " = ", ""] 這里是插值未生效的原始字符串數組 // 6 這里是第一個插值表達式的結果,也就是 a = 6 //9 第二個插值表達式的結果,也就是 b = 9 //15 第三個插值表達式的結果,也就是 a+b = 15console.log(untaggedResult); //-> "6 + 9 = 15" console.log(taggedResult); //-> "foobar"因為表達式的參數的數量是可變的,所以通常應該使用剩余操作符(rest operator)將它們收集到數組中:
let a=6,b=9; function simpleTag(strings,...expressions){console.log(strings);for(const exp of expressions){console.log(exp);}return 'foobar'; } //調用結果同上,不贅述對于有n個插值的模板字面量。傳給標簽函數的表達式參數個數始終是n,加上第一個參數則傳給標簽函數的參數始終是n+1。因此,如果想把這些字符串和對表達式求值的結果拼接起來作為默認返回的字符串,可以這樣做:
let a=6,b=9; function zipTag(strings,...expressions){return strings[0] + expressions.map((e,i) => `${e}${strings[i+1]}`).join('');//map():參數1表示當前元素的值;參數2表示當前元素的索引值//join():按照給定的字符串作為分隔符拼接整個數組//拼接思路:先將原始字符串數組的第一個元素單獨拿出來;將保存插值表達式結果的數組用map遍歷,返回的值為 “當前插值表達式結果” + “對應的下一個原始數組字符串” 所產生的表達式,最后用join拼接。/*例子的拼接:第一個原始字符串元素:"" ;插值表達式數組:第一次遍歷:6 + " + "; -> 返回 "6 + "第二次遍歷:9 + " = "; -> 返回 "9 + "第三次遍歷:15 + ""; -> 返回 "15"join拼接:"6 + 9 = 15"*/ } 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"7)原始字符串 *
使用模板字面量也可以直接獲取原始的模板字面量內容(如換行符和Unicode字符),而不是被轉換后的字符表示。為此,可以使用默認的String.raw標簽函數:
console.log(`\u00A9`); //-> ? 對應的Unicode字符:版權符 console.log(String.raw`\u00A9`); //-> \u00A9注意:原字符串中自帶轉義序列如換行符,可以直接獲取到。但是對實際的換行行為無用,它們不會被轉換成轉義序列的形式。
另外,可以通過標簽函數的第一個參數(即字符串數組)的**.raw屬性**取得每個字符串的原始內容。
function printRaw(strings){for(const rawStr of strings.raw)console.log(rawStr); } printRaw`\u00A9 ${'and'} \n`; //-> \u00A9 返回的是原始內容,而非對應的Unicode字符 //-> \n3.2.7 symbol類型 *
Symbol(符號)是ES6新增的數據類型。符號是原始值,且符號實例是唯一、不可變的。符號的用途是確保對象屬性使用唯一標識符,不會發生屬性沖突的危險。
1)符號的基本用法 *
符號需要使用Symbol()函數初始化。typeof操作符返回symobol:
let sym=Symbol(); console.log(typeof sym); //-> symbol可以傳入一個字符串參數作為對符號的描述。符號沒有字面量語法。
Symbol()函數不能用作構造函數,與new關鍵字一起使用。這樣避免創建符號包裝對象,像使用Boolean、String、Number一樣。如果確實想使用符號包裝對象,可以借用Object函數。
let sym=new Symbol(); console.log(sym);//-> TypeError: Symbol is not a constructorlet mySym=Symbol(); let myWrappedSym=Object(mySym); //使用Object()創建符號包裝對象 console.log(typeof myWrappedSym); //-> object2)使用全局符號注冊表 *
如果運行時的不同部分需要共享和重用符號實例,那么可以用一個字符串作為鍵,在全局符號注冊表中創建并重用符號。使用Symbol.for()函數。
Symbol.for()對每個字符串鍵都執行冪等操作。第一次使用某個字符串調用時,它會檢查全局運行時注冊表,發現不存在對應的符號,于是就會生成一個新符號實例并添加到注冊表中。后續使用相同的字符串的調用同樣會檢查注冊表,發現存在與該字符串對應的符號,然后就會返回該符號實例。
let fooGlobalSymbol=Symbol.for('foo'); //創建新符號 let otherFooGlobalSymbol=symbol.for('foo'); //重用已有符號 console.log(fooGlobalSymbol===otherFooGlobalSymbol);//->true//但是要注意,即便采用相同的符號描述,在全局注冊表中定義的符號和使用SYmbol()定義的符號也并不等同: let localSymbol=Symbol('foo'); console.log(localSymbol===fooGlobalSymbol); //-> false全局注冊表中的符號必須使用字符串鍵來創建,因此傳給Symbol.for()的任何值都會被轉換為字符串。注冊表中使用的鍵也會同時被用作符號描述。
還可以使用Symbol.keyFor()來查詢全局注冊表,這個方法接收符號,返回該全局符號對應的字符串鍵。若查詢的不是全局符號,則返回undefined。若查詢的不是符號,則會拋出TypeError。
3)使用符號作為屬性 *
凡是可以使用字符串或數值作為屬性的地方,都可以使用符號。這就包括了對象字面量屬性和Object.defineProperty() / object.definedProperties()定義的屬性。對象字面量只能在計算屬性語法中使用符號作為屬性。
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)] 返回的是常規屬性和符號屬性兩種的鍵因為符號屬性是對內存中符號的一個引用,所以直接創建并用作屬性的符號不會丟失。但是,如果沒有顯式地保存對這些屬性的引用,那么必須遍歷對象的所有符號屬性才能找到相應的屬性鍵:
//和上面不同,這里直接在對象中使用Symbol()創建了符號實例作為屬性,而沒有顯式的保存這些實例 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/)); congole.log(barSymbol); //-> Symbol(bar)4)常用內置符號 *
ECMAScript 6 也引入了一批常用內置符號(well-known symbol ),用于暴露語言內部行為,開發者可以直接訪問、重寫或模擬這些行為。這些內置符號都以Symbol工廠函數字符串屬性的形式存在。
這些內置符號最重要的用途之一是重新定義它們,從而改變原生結構的行為。比如,我們知道for-of 循環會在相關對象上使用Symbol.iterator屬性,那么就可以通過在自定義對象上重新定義Symbol.iterator的值,來改變for-of在迭代該對象時的行為。
這些內置符號也沒有什么特別之處,它們就是全局函數symbol的普通字符串屬性,指向一個符號的實例。所有內置符號屬性都是不可寫、不可枚舉、不可配置的。
注意:在提到ECMAScript規范時,經常會引用符號在規范中的名稱,前綴為@@。比如,@@giterator 指的就是Symbol.iterator。
PS:后續一些ES6內置的Symbol值,也即是常用內置符號,將不在此提及。這里有篇CSDN上簡單的總結:JavaScriptES6內置的Symbol值。(文章缺少書中提及的另一個內置符號:Symbol.asyncIterator。但是由于該屬性是ES2018規范的,因此只有版本非常新的瀏覽器才支持,所以也沒必要全了解。用到的話就百度吧。)
3.2.8 Obejct類型
使用 new Object() 新建一個對象。(可以省略括號,但不推薦)
Obeject類型的每個實例都具有下列屬性和方法:
- constructor:用于創建當前對象的函數。
- hasOwnProperty(propertyName):用于判斷當前對象實例中(不是原型)是否存在給定的屬性。其中,作為參數的屬性名必須以字符串形式存在(例如: o.hasOwnProperty("name"))。
- isPrototypeOf(object):用于檢查傳入的對象是否是另一個對象的原型。
- propertyIsEnumerable(*propertyName*):用于檢查給定的屬性是否能夠使用for-in語句來枚舉。參數必須以字符串形式存在。
- toLocalString():返回對象的字符串表示,該字符串與執行環境的地區對應。
- toString():返回對象的字符串表示。
- valueOf():返回對象的字符串、數值或布爾值表示。通常與toString()方法的返回值相同。
3.3 操作符
3.3.1 一元操作符
1)遞增遞減操作符 ++ / –
遞增遞減操作符直接照搬自C語言,且分為前置型和后置型。
使用前置型時,變量的值都是在語句被求值以前改變的**。且由于前置遞增和遞減操作與執行語句的優先級相等,因此整個語句會從左至右被求值。例如:
var num1 = 2; var num2 = 20; var num3 = --num1 + num2; //21 var num4 = num1 + num2; //21后置型遞增和遞減操作都是在包含它們的語句被求值后才執行的。
var num1 = 2; var num2 = 20; var num3 = num1-- + num2; //22,此時--還未執行 var num4 = num1 + num2; //21,使用了num1遞減后的值這些操作符適用任何類型的值。在應用不同的值時,遵循下列規則:
- 應用一個包含有效數字字符的字符串時,先將其轉化為數字值,再執行加減1的操作。字符串變量變成數值變量。
- 應用一個不包含有效數字的字符串時,將變量的值設置為NaN。字符串變量變為數值變量。
- 應用布爾值false時,先將其轉化為0再執行加減1的操作。布爾值變量變為數值變量。
- 應用布爾值true時,先將其轉化為1再執行加減1的操作。布爾值變量變為數值變量。
- 應用于對象時,(后面第5章會詳細介紹)先調用對象的valueOf()方法以取得一個可供操作的值。然后對該值應用前述規則。如果結果是NaN,則調用toString()方法后再應用前述規則。對象變量變成數值變量。
2)一元加減操作符 + / -
一元加操作符以一個加號表示,放在數值前不會產生任何影響。但應用在非數值時,該操作符會像Number()轉型函數一樣對這個值進行轉換。
一元減操作符應用于數值時,該值會變成負數。應用于非數值時,遵循與一元加操作符相同的規則,最后將值轉化為負數。
3.3.2 位操作符
位操作符用于數值的底層操作,即按內存中表示數值的位來操作數值。位操作符并不直接操作64位的值,而是先將64位轉化為32位的整數,然后執行操作,最后再將結果轉換回64位。
對于有符號的整數,32位中的前31位表示整數的值。第三十二位(即符號位)表示數值的符號:0表示正數,1表示負數。正數以純二進制格式存儲。
負數同樣以二進制碼存儲,但使用的格式是二進制補碼。計算補碼步驟:
1)按位非(NOT)
按位非操作符由一個波浪線(~)表示,執行按位非的結果就是返回數值的反碼。
var num1 = 25; //二進制00000000000000000000000000011001 var num2 = ~num1; //二進制1111111111111111111111111100110 alert(num2); //-26按位非操作的本質:操作數的負值減1。但相比負值減1的操作,由于按位非是在數值表示的最底層執行操作,因此操作速度更快。
2)按位與(AND)
按位與操作符由一個和號字符(&)表示,它有兩個操作符數。從本質上講,按位與操作就是將兩個數值的每一位對齊,然后根據***對應位都是1時才返回1,任何一位是0,結果都是0***的規則,對相同位置上的兩個數執行AND操作。例如:
var result = 25 & 3; alert(result); //1//底層操作:25 = 0000 0000 0000 0000 0000 0000 0001 10013 = 0000 0000 0000 0000 0000 0000 0000 0011 --------------------------------------------- AND = 0000 0000 0000 0000 0000 0000 0000 00013)按位或(OR)
按位或操作符由一個豎線符號(|)表示。同樣也有兩個操作數。根據***有一個位是1的情況下就返回1,只有兩個都是0的情況下才返回0***的規則執行OR操作。
var result = 25 | 3; alert(result); //274)按位異或(XOR)
按位異或由一個插入符號(^)表示。也有兩個操作數。遵循兩個數值***對應位上只有一個1時才返回1,如果對應的兩位都是1或都是0,則返回0***的規則。
5)左移
左移操作符由兩個小于號(<<)表示,這個操作符會將數值的所有位向左移動指定的位數。例如:
var oldValue = 2; //二進制碼10 var newValue = oldValue << 5; //二進制碼1000000 //向左位移后,原數值的右側多出了5個空位,左移操作會以0填充這些空位。注意:左移不會影響操作數的符號位。換言之,-2左移5位的結果是-64而非64。
6)右移
有符號的右移操作符由兩個大于號(>>)表示。這個操作符會將數值向右移動5位,但保留符號位(即正負號標記)。
同樣,在移位過程中,原數值也會出現空位,而這次的空位出現在原數值的左側、符號位的右側。而此時ECMAScript會用符號位的值來填充所有的空位。
7)無符號右移
無符號右移操作符以三個大于號(>>>)表示。這個操作符會將數值的所有32位都向右移動。對正數來說,無符號右移與有符號右移相同。
對于負數,無符號右移是以0填充空位而非以符號位的值。其次,無符號右移操作符會把負數的二進制碼當成正數的二進制碼。由于負數以其絕對值的二進制補碼形式表示,因此會導致無符號右移后的結果非常大。例如:
var oldValue = -64;//等于二進制111111111111111111111111000000 var newValue = oldValue >>> 5; //等于十進制134217726 //這里無符號右移操作符會將這個二進制碼當成正數的二進制碼,換算成十進制就是4294967232,將其右移5位,結果就變成了000001111111111111111111111110,即十進制的134217726。3.3.3 布爾操作符
1)邏輯非
邏輯非操作符由一個感嘆號(!)表示。無論這個值是什么數據類型,這個操作符都會返回一個布爾值然后對其求反。
同時使用兩個邏輯非操作符,實際上就會模擬Boolean()轉型函數的行為。
2)邏輯與
邏輯與操作符由兩個和號(&&)表示。邏輯與可以應用在任何類型的操作數,在有一個操作數不是布爾值的情況下,遵循下列規則:
- 第一個操作數是對象,此時返回第二個操作數;
- 第二個操作數是對象,則只有在第一個操作數的求值結果位是true的情況下才會返回該對象;
- 如果兩個操作數都是對象,則返回第二個操作數;
- 如果有一個操作數是null / NaN / undefined ,則返回null / NaN / undefined 。
邏輯與操作屬于短路操作。即若第一個操作數求值結果為false,就不會對第二個數進行求值了。
3)邏輯或
邏輯或操作符由兩個豎線符號(||)表示。邏輯或在有一個操作數不是布爾值的情況下遵循下列規則:
- 第一個操作數是對象,則返回第一個操作數;
- 第一個操作數的求值結果為false,則返回第二個操作數;
- 如果兩個操作數都是對象,則返回第一個對象
- 如果兩個操作數都是null / NaN / undefined ,則返回null / NaN / undefined 。
**邏輯或同屬短路操作。**即若第一個操作數求值結果為true,就不會對第二個數進行求值了。
我們可以利用邏輯或這一行為來避免為變量賦null或undefined值。例如:
var myObject = preferredObject || backupObject;//在這個例子中,變量myObject將被賦予等號后面兩個值中一個。變量preferredObject中包含優先賦給變量myObject的值,變量backupObject負責在preferredObject中不包含有效值的情況下提供后備值。PS:后面還有乘性操作符、加性操作符、關系操作符、相等操作符、賦值操作符、逗號操作符,就不再贅述。只要知道:
乘性操作符、加性操作符、關系操作符在操作數為非數值的情況下,執行運算時都可以在后臺轉換不同的數據類型。
相等操作符:簡單來說:相等( == )和不相等( != )操作符在操作數類型不同時會先進行強制類型轉換再比較;而全等( === )和全不等( !== )僅作比較而不會轉換。當然,涉及到對象的時候會復雜點,但是這里也沒必要多做討論。
在賦值時使用逗號操作符分隔值,最終會返回表達式中的最后一個值(這種使用場景并不多見,但確實存在):
let num = (5,1,2,3,0); //-> num的值會是0
3.4 語句
if、for、while、do-while、break、continue、switch語句這里不再提及。
因為不推薦with語句,所以這里也不再提及。with語句在嚴格模式下會報錯。
3.4.1 循環語句 `
for-in語句
語法如下:
for(property in expression) statement-
定義迭代變量時推薦使用const(就如之前使用一樣);
-
for-in不能保證返回對象屬性的順序;
-
如果要迭代的變量是null和undefined。則不執行循環體。
for-of語句
語法如下:
for(property of expression) statement- 定義迭代變量推薦使用const;
- for-of循環會按照可迭代對象的next()方法產生值得順序迭代元素。可迭代對象會在第7章介紹。
- 若嘗試迭代變量的不支持迭代,則語句會拋出錯誤。
注:ES2018對for-of語句,增加了for-await-of 循環,以支持生成期約(promise)的異步可迭代對象。(這個新增循環和前面提到的常用內置符號Symbol.asyncIterator有關系,可以自行了解)
3.4.2 標簽語句
使用label語句可以在代碼中添加標簽,以便將來使用。語法:
label : statement //實例: start : for (var i = 0; i < count; i++){alert(i); } //該例子中定義的start標簽可以在將來由break或continue語句引用。加標簽的語句一般都要與for語句等循環語句配合使用。3.5 函數
函數體中語句碰到return語句會立即停止執行并退出,后續代碼不會被執行。return語句也可以不帶返回值。這時候,函數會立即停止并返回undefined。這種用法最常用于提前終止函數執行。
嚴格模式下對函數有一些限制,若發生以下情況,會發生語法錯誤:
- 不能把函數命名為eval或arguments;
- 不能把函數的參數命名為eval或arguments;
- 不能出現兩個命名參數同名的情況。
第10章會更詳細的介紹函數。
總結
以上是生活随笔為你收集整理的《JavaScript高级程序设计(第四版)》红宝书学习笔记(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#数据结构-队列
- 下一篇: KMP算法的Next数组详解(转)