整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          《缺陷周話》第一期:空指針解引用

          《缺陷周話》第一期:空指針解引用

          代碼審計是使用靜態分析發現源代碼中安全缺陷的方法,能夠輔助開發或測試人員在軟件上線前較為全面地了解其安全問題,防患于未然,因此一直以來都是學術界和產業界研究的熱點,并且已經成為安全開發生命周期 SDL 和 DevSecOps 等保障體系的重要技術手段。

          360代碼衛士團隊基于自主研發的國內首款源代碼安全檢測商用工具,以及十余年漏洞技術研究的積累,推出《缺陷周話》系列欄目。每周針對 CWE、 OWASP 等標準中的一類缺陷,結合實例和工具使用進行詳細介紹,旨在為廣大開發和安全人員提供代碼審計的基礎性標準化教程。

          一、空指針解引用

          C語言空指針的值為NULL。一般NULL指針指向進程的最小地址,通常這個值為0。試圖通過空指針對數據進行訪問,會導致運行時錯誤。當程序試圖 解引用一個期望非空但是實際為空的指針時,會發生空指針解引用錯誤。對空指針的解引用會導致未定義的行為。在很多平臺上,解引用空指針可能會導致程序異常 終止或拒絕服務。如:在 Linux 系統中訪問空指針會產生 Segmentation fault 的錯誤。詳細請參見CWEID 476: NULL Pointer Dereference (http://cwe.mitre.org/data/definitions/476.html)。

          二、空指針解引用的危害

          空指針解引用是 C/C++ 程序中較為普遍的內存缺陷類型,當指針指向無效的內存地址并且對其引用時,有可能產生不可預見的錯誤,導致軟件系統崩潰。空指針引用缺陷可能導致系統崩 潰、拒絕服務等諸多嚴重后果。自2018年1月至9月,CVE 中共有100多條漏洞信息與其相關。其中包括18個Linux kernel 漏洞,部分漏洞如下:

          CVE 描述 CVE-2018-16517 Netwide Assembler 的 asm/labels.c 文件中存在空指針解引用,導致允許攻擊者進行拒絕服務攻擊。 CVE-2018-16517 Netwide Assembler 的 asm/labels.c 文件中存在空指針解引用,導致允許攻擊者進行拒絕服務攻擊。 CVE-2018-16428 GNOME Glib 2.56.1,gmarkup. 中的 g_markup_parse_context_end_parse() 函數存在一個空指針解引用。 CVE-2018-16329 ImageMagick 7.0.8-8 之前版本,MagickCore/property.c 文件中的 GetMagickProperty() 函數存在空指針解引用。 CVE-2018-16328 ImageMagick 7.0.8-8 之前版本,MagickCore/log.c 文件中的 CheckEventLogging () 函數存在空指針解引用。 三、示例代碼

          3.1 缺陷代碼

          上述示例代碼雖然在第16行對指針 p 進行了非空驗證,但由于驗證邏輯不完整,在第20行傳入函數 foo 中的指針p 仍可能為空指針。從而導致空指針解引用的發生。

          使用360代碼衛士對上述示例代碼進行檢測,可以檢出“空指針解引用”缺陷,顯示等級為。如圖1所示:

          圖1 空指針解引用檢測示例

          3.2 修復代碼

          在上述修復代碼中,在第19行對指針 p 為空的情況進行了處理,當指針p 為空時,函數npd_check_call_might()返回,避免了第23行傳入函數foo中的指針p 為空指針。

          使用 360 代碼衛士對修復后的代碼進行檢測,可以看到已不存在“空指針解引用”缺陷。如圖2:

          圖2:修復后檢測結果

          四、開源代碼檢測計劃某項目空指針解引用示例

          開源代碼檢測計劃是一項免費的公益計劃。通過使用360代碼衛士對開源項目進行源代碼檢測和審計,找到源代碼中存在的安全缺陷并及時與項目開發人員進行溝通和確認,使得開源項目的安全性得到提高。

          以下是開源項目檢測計劃中檢測出的一個“空指針解引用”示例,如圖3所示。

          圖3 開源項目檢出“空指針解引用”

          4.1 缺陷代碼

          在缺陷代碼中, 指針P在第133行通過ibwgetnode(iface)函數返回值進行賦值,并在第136行p->limit=speed; 中進行使用,通過ibwgetnode(iface) 函數的實現分析,可以看到在162行中返回 NULL 值,因此當p->limit=speed;操作時,由于在之前并沒有對P 是否為空進行判斷,導致空指針解引用。

          4.2修復代碼

          針對該空指針解引用問題的提出,開發人員在近期對相關代碼進行了修復。在139行對p進行解引用前,進行了非空判斷,從而避免了解引用空指針。

          五、如何避免空指針解引用

          為了避免空指針解引用的發生,需要在代碼編寫過程中養成良好的編程習慣,如:

          指針在使用前需要進行健壯性檢查,從而避免對空指針進行解引用操作;

          當調用函數的返回值可能為空時,需要對函數返回值進行非空驗證,從而避免空指針解引用;

          在釋放指針指向的空間后,需要將指針的值賦為空;

          確保異常被正確處理。

          *本文作者:360代碼衛士團隊,轉載請注明來自FreeBuf.COM。

          指針的本質是什么?它的風險是什么?它為什么如此特殊?我們應該如何閱讀指針相關的代碼?


          眾所周知,C/C++的精髓是指針,指針的本質是內存地址。可無論是普通變量,還是類的成員變量,誰還能沒個內存地址呢?既然普通變量有內存地址,那普通變量能像指針變量那樣,進行指針操作嗎?也就是指針的:* 操作和->操作。


          01

          代碼分析

          答案或許是肯定的!事實勝于雄辯,打開Compiler Explorer,定義一個普通的變量a,寫一個func1,借用指針變量p,通過 * 操作,來給變量 a 賦值;

          再寫一個func2,不借助指針變量,直接對變量a的地址,進行 * 操作,來給變量 a 賦值。

          int a=0;
          void func1()
          {
             int* p=&a;
             *p=1;
          }
          void func2()
          {
             *(int*)&a=1;
          }

          對比一下匯編指令:

          func1 用了3 條指令,指令的詳細分析,可以參看:CPU眼里的:指針變量;func2就簡潔多了,直接對變量a的內存地址寫:1;僅 1 條指令,就完成了賦值。

          如你所見,普通變量也能作指針操作。只要知道變量 a 的地址,就可以進行指針的 * 操作;還省掉了指針變量的開銷,更簡單、直接!

          當然,還可以把func2寫的更極端一些。假設,a 的內存地址是:0x1234,我們就可以把func2 改寫成這個樣子:

          int a=0;//a address=0x1234
          void func2()
          {
             //*(int*)&a=1;
             *(int*)0x1234=1;
          }

          或許,你會納悶,這還是讀寫變量嗎?這不就是在讀寫內存嗎?好吧,讓我們再寫一個循規蹈矩的func3,用最常規的方式,給變量 a 賦值:

          請問這個循規蹈矩的func3 跟 func2的匯編指令,有區別嗎?顯然,又是完全一致!

          所以,如你所見,你最熟悉的變量讀、寫,其本質還是等同于:對變量地址的指針 * 操作。正如變量的定義所言:變量不過是內存地址的別名。

          同樣的道理,類對象的“.”操作,跟“->”對應的指針操作,本質上也是相同的。


          02

          指針的風險

          當然,也正是指針操作的靈活性,也讓它成為:大神和黑客的最愛。很多大神,僅僅通過一個棧變量的地址,配合指針操作,就可以試探、回溯出完整的函數調用軌跡:

          雖然這很酷,但也會給程序安全留下了巨大的隱患,我們隨后的文章,還會對著名的堆棧攻擊,作詳細的解讀。

          同時,指針操作的不可控性,也是大規模編程的噩夢。試想一下:你得到某個私有變量的內存地址,就意味著你可以通過指針操作,不受任何限制的讀寫這個私有成員變量 x:

          這時,你還會理會:禁止從外部訪問的私有變量的語法規則嗎?你還愿意循規蹈矩的通過成員函數來讀寫它嗎?或許這個時候,任何語法層面的禁止,都會變成:一紙空文

          另外,除了地址:0x1234,指針操作,可以讀寫任意的內存地址:

          除了內存管理單元 MMU,沒有人能制止這種肆意的讀、寫行為。如果你的CPU不帶MMU,例如:STM32單片機,包括早期的DOS時代,程序員可以肆無忌憚的掃描整個計算機內存,并隨意修改,這也是早期游戲修改器的工作原理。

          所以,暴露任何數據、函數的內存地址,都是巨大的風險!因為這些地址,都可以用來做違規、不受控的指針操作。或許這也是大家對指針又愛又恨的原因吧!而當今比較流行的編程語言:C#、Java、JavaScript、Python、Rust,干脆就看不到內存地址和指針了。


          03

          總結

          1. 指針操作(*、->)不是指針變量的專利,普通變量,甚至立即數,也可以作指針操作。夸張的說:所有變量、對象的讀寫操作,都是基于指針,來實現的
          2. 計算機的世界里面,萬物皆有地址,所以,萬物皆可:指針。你即可能:循規蹈矩,通過變量名或規定的函數接口,讀寫變量,也可能:無視規則,通過指針操作,隨意、隨時、隨地的讀、寫變量。


          04

          熱點問題

          Q1:如何有效的閱讀指針相關的代碼?

          A1: 首先克服心理壓力,指針變量,就是普通變量,它叫什么名字并不重要!重要的是:它的值是多少?它的值對應了哪個:數據、變量、函數、或內存地址。

          指針的出現,往往意味著它要脫離常規的語法讀、寫規則,而用 “遠程” 的方式讀、寫某個:數據、變量、函數、或內存。


          Q2:為什么指針如此特殊?

          A2:與其說指針特殊,不如說指針的值比較特殊!如果我把數字:1234567貼在墻上,你可能覺得沒有什么,但如果這段數字是你的身份證號碼,你可能就抓狂了。

          內存是計算機中最重要的資源,我們所能看到、聽到的:圖像、視頻、聲音、計算結果無不來自內存,一旦獲取了內存地址,就相當于獲取了某塊內存唯一的身份證號碼。你不僅可以悄無聲息的隨時監控它的狀態,甚至可以修改它的內容。

          所以,指針是一個雙刃劍,它們能繞過各種語法規則的限制,簡單、高效、不被人知的訪問內存;同時,這也讓它成為C/C++最為讓人詬病的地方,這種過于強大、不受限的功能,往往讓程序的安全性變的不可控。


          04

          更多知識

          如果喜歡阿布這種解讀方式,希望更加系統學習這些編程知識的話,也可以考慮看看由阿布親自編寫,并有多位微軟大佬聯袂推薦的新書《CPU眼里的C/C++》

          小夕:https://juejin.im/post/5cab0c45f265da2513734390

          1. 基本類型有哪幾種?null 是對象嗎?基本數據類型和復雜數據類型存儲有什么區別?

          • 基本類型有6種,分別是undefined,null,bool,string,number,symbol(ES6新增)。
          • 雖然 typeof null 返回的值是 object,但是null不是對象,而是基本數據類型的一種。
          • 基本數據類型存儲在棧內存,存儲的是值。
          • 復雜數據類型的值存儲在堆內存,地址(指向堆中的值)存儲在棧內存。當我們把對象賦值給另外一個變量的時候,復制的是地址,指向同一塊內存空間,當其中一個對象改變時,另一個對象也會變化。

          2. typeof 是否正確判斷類型? instanceof呢? instanceof 的實現原理是什么?

          首先 typeof 能夠正確的判斷基本數據類型,但是除了 null, typeof null輸出的是對象。

          但是對象來說,typeof 不能正確的判斷其類型, typeof 一個函數可以輸出 'function',而除此之外,輸出的全是 object,這種情況下,我們無法準確的知道對象的類型。

          instanceof可以準確的判斷復雜數據類型,但是不能正確判斷基本數據類型。

          instanceof 是通過原型鏈判斷的,A instanceof B, 在A的原型鏈中層層查找,是否有原型等于B.prototype,如果一直找到A的原型鏈的頂端(null;即Object.__proto__.__proto__),仍然不等于B.prototype,那么返回false,否則返回true.

          instanceof的實現代碼:

          // L instanceof R
          function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
           var O=R.prototype;// 取 R 的顯式原型
           L=L.__proto__; // 取 L 的隱式原型
           while (true) { 
           if (L===null) //已經找到頂層
           return false; 
           if (O===L) //當 O 嚴格等于 L 時,返回 true
           return true; 
           L=L.__proto__; //繼續向上一層原型鏈查找
           } 
          }
          

          3. for of , for in 和 forEach,map 的區別。

          • for...of循環:具有 iterator 接口,就可以用for...of循環遍歷它的成員(屬性值)。for...of循環可以使用的范圍包括數組、Set 和 Map 結構、某些類似數組的對象、Generator 對象,以及字符串。for...of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。對于普通的對象,for...of結構不能直接使用,會報錯,必須部署了 Iterator 接口后才能使用。可以中斷循環。
          • for...in循環:遍歷對象自身的和繼承的可枚舉的屬性, 不能直接獲取屬性值。可以中斷循環。
          • forEach: 只能遍歷數組,不能中斷,沒有返回值(或認為返回值是undefined)。
          • map: 只能遍歷數組,不能中斷,返回值是修改后的數組。

          PS: Object.keys():返回給定對象所有可枚舉屬性的字符串數組。

          關于forEach是否會改變原數組的問題,有些小伙伴提出了異議,為此我寫了代碼測試了下(注意數組項是復雜數據類型的情況)。 除了forEach之外,map等API,也有同樣的問題。

          let arry=[1, 2, 3, 4];
          arry.forEach((item)=> {
           item *=10;
          });
          console.log(arry); //[1, 2, 3, 4]
          arry.forEach((item)=> {
           arry[1]=10; //直接操作數組
          });
          console.log(arry); //[ 1, 10, 3, 4 ]
          let arry2=[
           { name: "Yve" },
           { age: 20 }
          ];
          arry2.forEach((item)=> {
           item.name=10;
          });
          console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]
          

          如還不了解 iterator 接口或 for...of, 請先閱讀ES6文檔: Iterator 和 for...of 循環

          更多細節請戳: github.com/YvetteLau/B…


          4. 如何判斷一個變量是不是數組?

          • 使用 Array.isArray 判斷,如果返回 true, 說明是數組
          • 使用 instanceof Array 判斷,如果返回true, 說明是數組
          • 使用 Object.prototype.toString.call 判斷,如果值是 [object Array], 說明是數組
          • 通過 constructor 來判斷,如果是數組,那么 arr.constructor===Array. (不準確,因為我們可以指定 obj.constructor=Array)
          function fn() {
           console.log(Array.isArray(arguments)); //false; 因為arguments是類數組,但不是數組
           console.log(Array.isArray([1,2,3,4])); //true
           console.log(arguments instanceof Array); //fasle
           console.log([1,2,3,4] instanceof Array); //true
           console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
           console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
           console.log(arguments.constructor===Array); //false
           arguments.constructor=Array;
           console.log(arguments.constructor===Array); //true
           console.log(Array.isArray(arguments)); //false
          }
          fn(1,2,3,4);
          

          5. 類數組和數組的區別是什么?

          類數組:

          1)擁有length屬性,其它屬性(索引)為非負整數(對象中的索引會被當做字符串來處理);

          2)不具有數組所具有的方法;

          類數組是一個普通對象,而真實的數組是Array類型。

          常見的類數組有: 函數的參數 arguments, DOM 對象列表(比如通過 document.querySelectorAll 得到的列表), jQuery 對象 (比如 $("div")).

          類數組可以轉換為數組:

          //第一種方法
          Array.prototype.slice.call(arrayLike, start);
          //第二種方法
          [...arrayLike];
          //第三種方法:
          Array.from(arrayLike);
          

          PS: 任何定義了遍歷器(Iterator)接口的對象,都可以用擴展運算符轉為真正的數組。

          Array.from方法用于將兩類對象轉為真正的數組:類似數組的對象(array-like object)和可遍歷(iterable)的對象。


          6.==和===有什么區別?

          ===不需要進行類型轉換,只有類型相同并且值相等時,才返回 true.

          ==如果兩者類型不同,首先需要進行類型轉換。具體流程如下:

          1. 首先判斷兩者類型是否相同,如果相等,判斷值是否相等.
          2. 如果類型不同,進行類型轉換
          3. 判斷比較的是否是 null 或者是 undefined, 如果是, 返回 true .
          4. 判斷兩者類型是否為 string 和 number, 如果是, 將字符串轉換成 number
          5. 判斷其中一方是否為 boolean, 如果是, 將 boolean 轉為 number 再進行判斷
          6. 判斷其中一方是否為 object 且另一方為 string、number 或者 symbol , 如果是, 將 object 轉為原始類型再進行判斷
          let person1={
           age: 25
          }
          let person2=person1;
          person2.gae=20;
          console.log(person1===person2); //true,注意復雜數據類型,比較的是引用地址
          

          思考: []==![]

          我們來分析一下: []==![] 是true還是false?

          1. 首先,我們需要知道 ! 優先級是高于==(更多運算符優先級可查看: 運算符優先級)
          2. ![] 引用類型轉換成布爾值都是true,因此![]的是false
          3. 根據上面的比較步驟中的第五條,其中一方是 boolean,將 boolean 轉為 number 再進行判斷,false轉換成 number,對應的值是 0.
          4. 根據上面比較步驟中的第六條,有一方是 number,那么將object也轉換成Number,空數組轉換成數字,對應的值是0.(空數組轉換成數字,對應的值是0,如果數組中只有一個數字,那么轉成number就是這個數字,其它情況,均為NaN)
          5. 0==0; 為true

          7. ES6中的class和ES5的類有什么區別?

          1. ES6 class 內部所有定義的方法都是不可枚舉的;
          2. ES6 class 必須使用 new 調用;
          3. ES6 class 不存在變量提升;
          4. ES6 class 默認即是嚴格模式;
          5. ES6 class 子類必須在父類的構造函數中調用super(),這樣才有this對象;ES5中類繼承的關系是相反的,先有子類的this,然后用父類的方法應用在this上。

          8. 數組的哪些API會改變原數組?

          修改 原數組的API有:

          splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

          不修改 原數組的API有:

          slice/map/forEach/every/filter/reduce/entries/find

          注: 數組的每一項是簡單數據類型,且未直接操作數組的情況下(稍后會對此題重新作答)。


          9. let、const 以及 var 的區別是什么?

          • let 和 const 定義的變量不會出現變量提升,而 var 定義的變量會提升。
          • let 和 const 是JS中的塊級作用域
          • let 和 const 不允許重復聲明(會拋出錯誤)
          • let 和 const 定義的變量在定義語句之前,如果使用會拋出錯誤(形成了暫時性死區),而 var 不會。
          • const 聲明一個只讀的常量。一旦聲明,常量的值就不能改變(如果聲明是一個對象,那么不能改變的是對象的引用地址)

          10. 在JS中什么是變量提升?什么是暫時性死區?

          變量提升就是變量在聲明之前就可以使用,值為undefined。

          在代碼塊內,使用 let/const 命令聲明變量之前,該變量都是不可用的(會拋出錯誤)。這在語法上,稱為“暫時性死區”。暫時性死區也意味著 typeof 不再是一個百分百安全的操作。

          typeof x; // ReferenceError(暫時性死區,拋錯)
          let x;
          復制代碼
          typeof y; // 值是undefined,不會報錯
          

          暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。


          11. 如何正確的判斷this? 箭頭函數的this是什么?

          this的綁定規則有四種:默認綁定,隱式綁定,顯式綁定,new綁定.

          1. 函數是否在 new 中調用(new綁定),如果是,那么 this 綁定的是新創建的對象【前提是構造函數中沒有返回對象或者是function,否則this指向返回的對象/function】。
          2. 函數是否通過 call,apply 調用,或者使用了 bind (即硬綁定),如果是,那么this綁定的就是指定的對象。
          3. 函數是否在某個上下文對象中調用(隱式綁定),如果是的話,this 綁定的是那個上下文對象。一般是 obj.foo()
          4. 如果以上都不是,那么使用默認綁定。如果在嚴格模式下,則綁定到 undefined,否則綁定到全局對象。
          5. 如果把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind, 這些值在調用時會被忽略,實際應用的是默認綁定規則。
          6. 箭頭函數沒有自己的 this, 它的this繼承于上一層代碼塊的this。

          測試下是否已經成功Get了此知識點(瀏覽器執行環境):

          var number=5;
          var obj={
           number: 3,
           fn1: (function () {
           var number;
           this.number *=2;
           number=number * 2;
           number=3;
           return function () {
           var num=this.number;
           this.number *=2;
           console.log(num);
           number *=3;
           console.log(number);
           }
           })()
          }
          var fn1=obj.fn1;
          fn1.call(null);
          obj.fn1();
          console.log(window.number);
          


          12. 詞法作用域和this的區別。

          • 詞法作用域是由你在寫代碼時將變量和塊作用域寫在哪里來決定的
          • this 是在調用時被綁定的,this 指向什么,完全取決于函數的調用位置.

          13. 談談你對JS執行上下文棧和作用域鏈的理解。

          執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境, JS執行上下文棧可以認為是一個存儲函數調用的棧結構,遵循先進后出的原則。

          • JavaScript執行在單線程上,所有的代碼都是排隊執行。
          • 一開始瀏覽器執行全局的代碼時,首先創建全局的執行上下文,壓入執行棧的頂部。
          • 每當進入一個函數的執行就會創建函數的執行上下文,并且把它壓入執行棧的頂部。當前函數執行-完成后,當前函數的執行上下文出棧,并等待垃圾回收。
          • 瀏覽器的JS執行引擎總是訪問棧頂的執行上下文。
          • 全局上下文只有唯一的一個,它在瀏覽器關閉時出棧。

          作用域鏈: 無論是 LHS 還是 RHS 查詢,都會在當前的作用域開始查找,如果沒有找到,就會向上級作用域繼續查找目標標識符,每次上升一個作用域,一直到全局作用域為止。


          14. 什么是閉包?閉包的作用是什么?閉包有哪些使用場景?

          閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包最常用的方式就是在一個函數內部創建另一個函數。

          閉包的作用有:

          1. 封裝私有變量
          2. 模仿塊級作用域(ES5中沒有塊級作用域)
          3. 實現JS的模塊

          15. call、apply有什么區別?call,aplly和bind的內部是如何實現的?

          call 和 apply 的功能相同,區別在于傳參的方式不一樣:

          • fn.call(obj, arg1, arg2, ...),調用一個函數, 具有一個指定的this值和分別地提供的參數(參數的列表)。
          • fn.apply(obj, [argsArray]),調用一個函數,具有一個指定的this值,以及作為一個數組(或類數組對象)提供的參數。

          call核心:

          • 將函數設為傳入參數的屬性
          • 指定this到函數并傳入給定參數執行函數
          • 如果不傳入參數或者參數為null,默認指向為 window / global
          • 刪除參數上的函數
          Function.prototype.call=function (context) {
           /** 如果第一個參數傳入的是 null 或者是 undefined, 那么指向this指向 window/global */
           /** 如果第一個參數傳入的不是null或者是undefined, 那么必須是一個對象 */
           if (!context) {
           //context為null或者是undefined
           context=typeof window==='undefined' ? global : window;
           }
           context.fn=this; //this指向的是當前的函數(Function的實例)
           let rest=[...arguments].slice(1);//獲取除了this指向對象以外的參數, 空數組slice后返回的仍然是空數組
           let result=context.fn(...rest); //隱式綁定,當前函數的this指向了context.
           delete context.fn;
           return result;
          }
          //測試代碼
          var foo={
           name: 'Selina'
          }
          var name='Chirs';
          function bar(job, age) {
           console.log(this.name);
           console.log(job, age);
          }
          bar.call(foo, 'programmer', 20);
          // Selina programmer 20
          bar.call(null, 'teacher', 25);
          // 瀏覽器環境: Chirs teacher 25; node 環境: undefined teacher 25
          

          apply:

          apply的實現和call很類似,但是需要注意他們的參數是不一樣的,apply的第二個參數是數組或類數組.

          Function.prototype.apply=function (context, rest) {
           if (!context) {
           //context為null或者是undefined時,設置默認值
           context=typeof window==='undefined' ? global : window;
           }
           context.fn=this;
           let result;
           if(rest===undefined || rest===null) {
           //undefined 或者 是 null 不是 Iterator 對象,不能被 ...
           result=context.fn(rest);
           }else if(typeof rest==='object') {
           result=context.fn(...rest);
           }
           delete context.fn;
           return result;
          }
          var foo={
           name: 'Selina'
          }
          var name='Chirs';
          function bar(job, age) {
           console.log(this.name);
           console.log(job, age);
          }
          bar.apply(foo, ['programmer', 20]);
          // Selina programmer 20
          bar.apply(null, ['teacher', 25]);
          // 瀏覽器環境: Chirs programmer 20; node 環境: undefined teacher 25
          

          bind

          bind 和 call/apply 有一個很重要的區別,一個函數被 call/apply 的時候,會直接調用,但是 bind 會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之后的一序列參數將會在傳遞的實參前傳入作為它的參數。

          Function.prototype.bind=function(context) {
           if(typeof this !=="function"){
           throw new TypeError("not a function");
           }
           let self=this;
           let args=[...arguments].slice(1);
           function Fn() {};
           Fn.prototype=this.prototype;
           let bound=function() {
           let res=[...args, ...arguments]; //bind傳遞的參數和函數調用時傳遞的參數拼接
           context=this instanceof Fn ? this : context || this;
           return self.apply(context, res);
           }
           //原型鏈
           bound.prototype=new Fn();
           return bound;
          }
          var name='Jack';
          function person(age, job, gender){
           console.log(this.name , age, job, gender);
          }
          var Yve={name : 'Yvette'};
          let result=person.bind(Yve, 22, 'enginner')('female');	
          


          16. new的原理是什么?通過new的方式創建對象和通過字面量創建有什么區別?

          new:

          1. 創建一個新對象。
          2. 這個新對象會被執行[[原型]]連接。
          3. 屬性和方法被加入到 this 引用的對象中。并執行了構造函數中的方法.
          4. 如果函數沒有返回其他對象,那么this指向這個新對象,否則this指向構造函數中返回的對象。
          function new(func) {
           let target={};
           target.__proto__=func.prototype;
           let res=func.call(target);
           if (res && typeof(res)=="object" || typeof(res)=="function") {
           	return res;
           }
           return target;
          }
          

          字面量創建對象,不會調用 Object構造函數, 簡潔且性能更好;

          new Object() 方式創建對象本質上是方法調用,涉及到在proto鏈中遍歷該方法,當找到該方法后,又會生產方法調用必須的 堆棧信息,方法調用結束后,還要釋放該堆棧,性能不如字面量的方式。

          通過對象字面量定義對象時,不會調用Object構造函數。


          17. 談談你對原型的理解?

          在 JavaScript 中,每當定義一個對象(函數也是對象)時候,對象中都會包含一些預定義的屬性。其中每個函數對象都有一個prototype 屬性,這個屬性指向函數的原型對象。使用原型對象的好處是所有對象實例共享它所包含的屬性和方法。


          18. 什么是原型鏈?【原型鏈解決的是什么問題?】

          原型鏈解決的主要是繼承問題。

          每個對象擁有一個原型對象,通過 proto (讀音: dunder proto) 指針指向其原型對象,并從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null(Object.proptotype.__proto__ 指向的是null)。這種關系被稱為原型鏈 (prototype chain),通過原型鏈一個對象可以擁有定義在其他對象中的屬性和方法。

          構造函數 Parent、Parent.prototype 和 實例 p 的關系如下:(p.__proto__===Parent.prototype)

          19. prototype 和 __proto__ 區別是什么?

          prototype是構造函數的屬性。

          __proto__ 是每個實例都有的屬性,可以訪問 [[prototype]] 屬性。

          實例的__proto__ 與其構造函數的prototype指向的是同一個對象。

          function Student(name) {
           this.name=name;
          }
          Student.prototype.setAge=function(){
           this.age=20;
          }
          let Jack=new Student('jack');
          console.log(Jack.__proto__);
          //console.log(Object.getPrototypeOf(Jack));;
          console.log(Student.prototype);
          console.log(Jack.__proto__===Student.prototype);//true
          

          20. 使用ES5實現一個繼承?

          組合繼承(最常用的繼承方式)

          function SuperType(name) {
           this.name=name;
           this.colors=['red', 'blue', 'green'];
          }
          SuperType.prototype.sayName=function() {
           console.log(this.name);
          }
          function SubType(name, age) {
           SuperType.call(this, name);
           this.age=age;
          }
          SubType.prototype=new SuperType();
          SubType.prototype.constructor=SubType;
          SubType.prototype.sayAge=function() {
           console.log(this.age);
          }
          
          

          其它繼承方式實現,可以參考《JavaScript高級程序設計》


          21. 什么是深拷貝?深拷貝和淺拷貝有什么區別?

          淺拷貝是指只復制第一層對象,但是當對象的屬性是引用類型時,實質復制的是其引用,當引用指向的值改變時也會跟著變化。

          深拷貝復制變量值,對于非基本類型的變量,則遞歸至基本類型變量后,再復制。深拷貝后的對象與原來的對象是完全隔離的,互不影響,對一個對象的修改并不會影響另一個對象。

          實現一個深拷貝:

          function deepClone(obj) { //遞歸拷貝
           if(obj===null) return null; //null 的情況
           if(obj instanceof RegExp) return new RegExp(obj);
           if(obj instanceof Date) return new Date(obj);
           if(typeof obj !=='object') {
           //如果不是復雜數據類型,直接返回
           return obj;
           }
           /**
           * 如果obj是數組,那么 obj.constructor 是 [Function: Array]
           * 如果obj是對象,那么 obj.constructor 是 [Function: Object]
           */
           let t=new obj.constructor();
           for(let key in obj) {
           //如果 obj[key] 是復雜數據類型,遞歸
           t[key]=deepClone(obj[key]);
           }
           return t;
          }
          


          22. 防抖和節流的區別是什么?防抖和節流的實現。

          防抖和節流的作用都是防止函數多次調用。區別在于,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小于設置的時間,防抖的情況下只會調用一次,而節流的情況會每隔一定時間調用一次函數。

          防抖(debounce): n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間

          function debounce(func, wait, immediate=true) {
           let timer;
           // 延遲執行函數
           const later=(context, args)=> setTimeout(()=> {
           timer=null;// 倒計時結束
           if (!immediate) {
           func.apply(context, args);
           //執行回調
           context=args=null;
           }
           }, wait);
           let debounced=function (...params) {
           let context=this;
           let args=params;
           if (!timer) {
           timer=later(context, args);
           if (immediate) {
           //立即執行
           func.apply(context, args);
           }
           } else {
           clearTimeout(timer);
           //函數在每個等待時延的結束被調用
           timer=later(context, args);
           }
           }
           debounced.cancel=function () {
           clearTimeout(timer);
           timer=null;
           };
           return debounced;
          };
          

          防抖的應用場景:

          • 每次 resize/scroll 觸發統計事件
          • 文本輸入的驗證(連續輸入文字后發送 AJAX 請求進行驗證,驗證一次就好)

          節流(throttle): 高頻事件在規定時間內只會執行一次,執行一次后,只有大于設定的執行周期后才會執行第二次。

          //underscore.js
          function throttle(func, wait, options) {
           var timeout, context, args, result;
           var previous=0;
           if (!options) options={};
           var later=function () {
           previous=options.leading===false ? 0 : Date.now() || new Date().getTime();
           timeout=null;
           result=func.apply(context, args);
           if (!timeout) context=args=null;
           };
           var throttled=function () {
           var now=Date.now() || new Date().getTime();
           if (!previous && options.leading===false) previous=now;
           var remaining=wait - (now - previous);
           context=this;
           args=arguments;
           if (remaining <=0 || remaining > wait) {
           if (timeout) {
           clearTimeout(timeout);
           timeout=null;
           }
           previous=now;
           result=func.apply(context, args);
           if (!timeout) context=args=null;
           } else if (!timeout && options.trailing !==false) {
           // 判斷是否設置了定時器和 trailing
           timeout=setTimeout(later, remaining);
           }
           return result;
           };
           throttled.cancel=function () {
           clearTimeout(timeout);
           previous=0;
           timeout=context=args=null;
           };
           return throttled;
          };
          

          函數節流的應用場景有:

          • DOM 元素的拖拽功能實現(mousemove)
          • 射擊游戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
          • 計算鼠標移動的距離(mousemove)
          • Canvas 模擬畫板功能(mousemove)
          • 搜索聯想(keyup)
          • 監聽滾動事件判斷是否到頁面底部自動加載更多:給 scroll 加了 debounce 后,只有用戶停止滾動后,才會判斷是否到了頁面底部;如果是 throttle 的話,只要頁面滾動就會間隔一段時間判斷一次

          23. 取數組的最大值(ES5、ES6)

          // ES5 的寫法
          Math.max.apply(null, [14, 3, 77, 30]);
          // ES6 的寫法
          Math.max(...[14, 3, 77, 30]);
          // reduce
          [14,3,77,30].reduce((accumulator, currentValue)=>{
           return accumulator=accumulator > currentValue ? accumulator : currentValue
          });
          

          24. ES6新的特性有哪些?

          1. 新增了塊級作用域(let,const)
          2. 提供了定義類的語法糖(class)
          3. 新增了一種基本數據類型(Symbol)
          4. 新增了變量的解構賦值
          5. 函數參數允許設置默認值,引入了rest參數,新增了箭頭函數
          6. 數組新增了一些API,如 isArray / from / of 方法;數組實例新增了 entries(),keys() 和 values() 等方法
          7. 對象和數組新增了擴展運算符
          8. ES6 新增了模塊化(import/export)
          9. ES6 新增了 Set 和 Map 數據結構
          10. ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例
          11. ES6 新增了生成器(Generator)和遍歷器(Iterator)

          25. setTimeout倒計時為什么會出現誤差?

          setTimeout() 只是將事件插入了“任務隊列”,必須等當前代碼(執行棧)執行完,主線程才會去執行它指定的回調函數。要是當前代碼消耗時間很長,也有可能要等很久,所以并沒辦法保證回調函數一定會在 setTimeout() 指定的時間執行。所以, setTimeout() 的第二個參數表示的是最少時間,并非是確切時間。

          HTML5標準規定了 setTimeout() 的第二個參數的最小值不得小于4毫秒,如果低于這個值,則默認是4毫秒。在此之前。老版本的瀏覽器都將最短時間設為10毫秒。另外,對于那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常是間隔16毫秒執行。這時使用 requestAnimationFrame() 的效果要好于 setTimeout();


          26. 為什么 0.1 + 0.2 !=0.3 ?

          0.1 + 0.2 !=0.3 是因為在進制轉換和進階運算的過程中出現精度損失。

          下面是詳細解釋:

          JavaScript使用 Number 類型表示數字(整數和浮點數),使用64位表示一個數字。

          圖片說明:

          • 第0位:符號位,0表示正數,1表示負數(s)
          • 第1位到第11位:儲存指數部分(e)
          • 第12位到第63位:儲存小數部分(即有效數字)f

          計算機無法直接對十進制的數字進行運算, 需要先對照 IEEE 754 規范轉換成二進制,然后對階運算。

          1.進制轉換

          0.1和0.2轉換成二進制后會無限循環

          0.1 -> 0.0001100110011001...(無限循環)
          0.2 -> 0.0011001100110011...(無限循環)
          

          但是由于IEEE 754尾數位數限制,需要將后面多余的位截掉,這樣在進制之間的轉換中精度已經損失。

          2.對階運算

          由于指數位數不相同,運算時需要對階運算 這部分也可能產生精度損失。

          按照上面兩步運算(包括兩步的精度損失),最后的結果是

          0.0100110011001100110011001100110011001100110011001100

          結果轉換成十進制之后就是 0.30000000000000004。

          27. promise 有幾種狀態, Promise 有什么優缺點 ?

          promise有三種狀態: fulfilled, rejected, pending.

          Promise 的優點:

          1. 一旦狀態改變,就不會再變,任何時候都可以得到這個結果
          2. 可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數

          Promise 的缺點:

          1. 無法取消 Promise
          2. 當處于pending狀態時,無法得知目前進展到哪一個階段

          28. Promise構造函數是同步還是異步執行,then中的方法呢 ?promise如何實現then處理 ?

          Promise的構造函數是同步執行的。then 中的方法是異步執行的。


          29. Promise和setTimeout的區別 ?

          Promise 是微任務,setTimeout 是宏任務,同一個事件循環中,promise.then總是先于 setTimeout 執行。


          30. 如何實現 Promise.all ?

          要實現 Promise.all,首先我們需要知道 Promise.all 的功能:

          1. 如果傳入的參數是一個空的可迭代對象,那么此promise對象回調完成(resolve),只有此情況,是同步執行的,其它都是異步返回的。
          2. 如果傳入的參數不包含任何 promise,則返回一個異步完成. promises 中所有的promise都“完成”時或參數中不包含 promise 時回調完成。
          3. 如果參數中有一個promise失敗,那么Promise.all返回的promise對象失敗
          4. 在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個數組
          Promise.all=function (promises) {
           return new Promise((resolve, reject)=> {
           let index=0;
           let result=[];
           if (promises.length===0) {
           resolve(result);
           } else {
           function processValue(i, data) {
           result[i]=data;
           if (++index===promises.length) {
           resolve(result);
           }
           }
           for (let i=0; i < promises.length; i++) {
           //promises[i] 可能是普通值
           Promise.resolve(promises[i]).then((data)=> {
           processValue(i, data);
           }, (err)=> {
           reject(err);
           return;
           });
           }
           }
           });
          }
          


          31.如何實現 Promise.finally ?

          不管成功還是失敗,都會走到finally中,并且finally之后,還可以繼續then。并且會將值原封不動的傳遞給后面的then.

          Promise.prototype.finally=function (callback) {
           return this.then((value)=> {
           return Promise.resolve(callback()).then(()=> {
           return value;
           });
           }, (err)=> {
           return Promise.resolve(callback()).then(()=> {
           throw err;
           });
           });
          }
          

          32. 什么是函數柯里化?實現 sum(1)(2)(3) 返回結果是1,2,3之和。

          函數柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。

          function sum(a) {
           return function(b) {
           return function(c) {
           return a+b+c;
           }
           }
          }
          console.log(sum(1)(2)(3)); // 6
          

          引申:實現一個curry函數,將普通函數進行柯里化:


          主站蜘蛛池模板: 亚洲av色香蕉一区二区三区蜜桃| 亚洲一区二区三区在线网站| 久久久精品人妻一区亚美研究所 | 性无码免费一区二区三区在线| 国产一区二区三区91| 久久精品岛国av一区二区无码| 91精品一区二区三区久久久久| 一色一伦一区二区三区| 日本在线视频一区| 国产精品美女一区二区视频| 伊人久久精品无码麻豆一区| 亚州国产AV一区二区三区伊在| 水蜜桃av无码一区二区| 国产精品无码一区二区在线观| 国产精品无码一区二区三区在| 国产福利电影一区二区三区久久老子无码午夜伦不 | 国产99精品一区二区三区免费| 亚洲AV本道一区二区三区四区| 国产精品乱码一区二区三| 久久精品无码一区二区日韩AV| 少妇无码一区二区三区免费| 国产综合精品一区二区| 另类免费视频一区二区在线观看| 2021国产精品视频一区| 国产激情无码一区二区三区| 88国产精品视频一区二区三区| 日本无卡码免费一区二区三区| 亚洲国产精品一区二区成人片国内| 海角国精产品一区一区三区糖心| 国产在线精品一区二区夜色 | 一区二区三区国模大胆| 亚洲中文字幕在线无码一区二区| 美女视频一区二区| 97一区二区三区四区久久| 国产美女露脸口爆吞精一区二区| 日韩免费一区二区三区在线播放 | 午夜福利一区二区三区在线观看 | 精品在线一区二区| 波多野结衣一区二区| 亚洲一区二区三区自拍公司| 日本人的色道www免费一区|