面試 你必需要懂的原生JS知識點

上篇
html

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,這種狀況下,咱們沒法準確的知道對象的類型。node

instanceof能夠準確的判斷複雜數據類型,可是不能正確判斷基本數據類型。nginx

instanceof 是經過原型鏈判斷的,A instanceof B, 在A的原型鏈中層層查找,是否有原型等於B.prototype,若是一直找到A的原型鏈的頂端(null;即Object.prototype.__proto__),仍然不等於B.prototype,那麼返回false,不然返回true.git

instanceof的實現代碼:github

// 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():返回給定對象全部可枚舉web

屬性的字符串數組

關於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 } ]複製代碼


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. 類數組和數組的區別是什麼?

類數組:express

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

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

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

常見的類數組有: 函數的參數 arugments, 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 綁定的是新建立的對象。
  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);複製代碼

若是this的知識點,您還不太懂,請戳: 你真的懂this嗎?

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

  • 詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的
  • 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. 若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。
function new(func) {
    let target = {};
    target.__proto__ = func.prototype;
    let res = func.call(target);
    if (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() {
    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 timeout, context, args;
        // 延遲執行函數
        const later = () => setTimeout(() => {
            // 延遲函數執行完畢,清空定時器
            timeout = null
            // 延遲執行的狀況下,函數會在延遲函數中執行
            // 使用到以前緩存的參數和上下文
            if (!immediate) {
                func.apply(context, args);
                context = args = null;
            }
        }, wait);
        let debounced = function (...params) {
            if (!timeout) {
                timeout = later();
                if (immediate) {
                    //當即執行
                    func.apply(this, params);
                } else {
                    //閉包
                    context = this;
                    args = params;
                }
            } else {
                clearTimeout(timeout);
                timeout = later();
            }
        }
    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = 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. 取數組的最大值(ES五、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 中的方法是異步執行的。

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函數,將普通函數進行柯里化:

function curry(fn, args = []) {
    return function(){
        let rest = [...args, ...arguments];
        if (rest.length < fn.length) {
            return curry.call(this,fn,rest);
        }else{
            return fn.apply(this,rest);
        }
    }
}
//test
function sum(a,b,c) {
    return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6複製代碼

本文轉載自 前端小姐姐




下篇

1.說一說JS異步發展史

異步最先的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。可是回調函數有一個很常見的問題,就是回調地獄的問題(稍後會舉例說明);

爲了解決回調地獄的問題,社區提出了Promise解決方案,ES6將其寫進了語言標準。Promise解決了回調地獄的問題,可是Promise也存在一些問題,如錯誤不能被try catch,並且使用Promise的鏈式調用,其實並無從根本上解決回調地獄的問題,只是換了一種寫法。

ES6中引入 Generator 函數,Generator是一種異步編程解決方案,Generator 函數是協程在 ES6 的實現,最大特色就是能夠交出函數的執行權,Generator 函數能夠看出是異步任務的容器,須要暫停的地方,都用yield語句註明。可是 Generator 使用起來較爲複雜。

ES7又提出了新的異步解決方案:async/await,async是 Generator 函數的語法糖,async/await 使得異步代碼看起來像同步代碼,異步編程發展的目標就是讓異步邏輯的代碼看起來像同步同樣。

1.回調函數: callback

//node讀取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
    //code
});複製代碼

回調函數的使用場景(包括但不限於):

  1. 事件回調
  2. Node API
  3. setTimeout/setInterval中的回調函數

異步回調嵌套會致使代碼難以維護,而且不方便統一處理錯誤,不能try catch 和 回調地獄(如先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C...)。

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});複製代碼

2.Promise

Promise 主要解決了回調地獄的問題,Promise 最先由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。

那麼咱們看看Promise是如何解決回調地獄問題的,仍然以上文的readFile爲例。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});複製代碼

想要運行代碼看效果,請戳(VS的 Code Runner 執行代碼): github.com/YvetteLau/B…

思考一下在Promise以前,你是如何處理異步併發問題的,假設有這樣一個需求:讀取三個文件內容,都讀取成功後,輸出最終的結果。有了Promise以後,又如何處理呢?代碼可戳: github.com/YvetteLau/B…

注: 可使用 bluebird 將接口 promise化;

引伸: Promise有哪些優勢和問題呢?

3.Generator

Generator 函數是 ES6 提供的一種異步編程解決方案,整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用 yield 語句註明。

Generator 函數通常配合 yield 或 Promise 使用。Generator函數返回的是迭代器。對生成器和迭代器不瞭解的同窗,請自行補習下基礎。下面咱們看一下 Generator 的簡單使用:

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值
t.next(1); //第一次調用next函數時,傳遞的參數無效
t.next(2); //a輸出2;
t.next(3); //b輸出2; 
t.next(4); //c輸出3;
t.next(5); //d輸出3;複製代碼

爲了讓你們更好的理解上面代碼是如何執行的,我畫了一張圖,分別對應每一次的next方法調用:


仍然以上文的readFile爲例,使用 Generator + co庫來實現:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
    //code
}).catch(err => {
    //code
});
複製代碼

不使用co庫,如何實現?可否本身寫一個最簡的my_co?請戳: github.com/YvetteLau/B…

PS: 若是你還不太瞭解 Generator/yield,建議閱讀ES6相關文檔。

4.async/await

ES7中引入了 async/await 概念。async實際上是一個語法糖,它的實現就是將Generator函數和自動執行器(co),包裝在一個函數中。

async/await 的優勢是代碼清晰,不用像 Promise 寫不少 then 鏈,就能夠處理回調地獄的問題。錯誤能夠被try catch。

仍然以上文的readFile爲例,使用 Generator + co庫來實現:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);


async function read() {
    await readFile(A, 'utf-8');
    await readFile(B, 'utf-8');
    await readFile(C, 'utf-8');
    //code
}

read().then((data) => {
    //code
}).catch(err => {
    //code
});複製代碼

可執行代碼,請戳:github.com/YvetteLau/B…

思考一下 async/await 如何處理異步併發問題的? github.com/YvetteLau/B…

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:說一說JS異步發展史

2.談談對 async/await 的理解,async/await 的實現原理是什麼?

async/await 就是 Generator 的語法糖,使得異步操做變得更加方便。來張圖對比一下:


async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成await。

咱們說 async 是 Generator 的語法糖,那麼這個糖究竟甜在哪呢?

1)async函數內置執行器,函數調用以後,會自動執行,輸出最後結果。而Generator須要調用next或者配合co模塊使用。

2)更好的語義,async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

3)更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async 函數的 await 命令後面,能夠是 Promise 對象和原始類型的值。

4)返回值是Promise,async函數的返回值是 Promise 對象,Generator的返回值是 Iterator,Promise 對象使用起來更加方便。

async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。

具體代碼試下以下(和spawn的實現略有差別,我的以爲這樣寫更容易理解),若是你想知道如何一步步寫出 my_co ,可戳: github.com/YvetteLau/B…

function my_co(it) {
    return new Promise((resolve, reject) => {
        function next(data) {
            try {
                var { value, done } = it.next(data);
            }catch(e){
                return reject(e);
            }
            if (!done) { 
                //donetrue,表示迭代完成
                //value 不必定是 Promise,多是一個普通值。使用 Promise.resolve 進行包裝。
                Promise.resolve(value).then(val => {
                    next(val);
                }, reject);
            } else {
                resolve(value);
            }
        }
        next(); //執行一次next
    });
}
function* test() {
    yield new Promise((resolve, reject) => {
        setTimeout(resolve, 100);
    });
    yield new Promise((resolve, reject) => {
        // throw Error(1);
        resolve(10)
    });
    yield 10;
    return 1000;
}

my_co(test()).then(data => {
    console.log(data); //輸出1000
}).catch((err) => {
    console.log('err: ', err);
});複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:談談對 async/await 的理解,async/await 的實現原理是什麼?

3.使用 async/await 須要注意什麼?

  1. await 命令後面的Promise對象,運行結果多是 rejected,此時等同於 async 函數返回的 Promise 對象被reject。所以須要加上錯誤處理,能夠給每一個 await 後的 Promise 增長 catch 方法;也能夠將 await 的代碼放在 try...catch 中。
  2. 多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。
//下面兩種寫法均可以同時觸發
//法一
async function f1() {
    await Promise.all([
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        }),
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        })
    ])
}
//法二
async function f2() {
    let fn1 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        });
    
    let fn2 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        })
    await fn1;
    await fn2;
}複製代碼
  1. await命令只能用在async函數之中,若是用在普通函數,會報錯。
  2. async 函數能夠保留運行堆棧。
/**
* 函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。
* 等到b()運行結束,可能a()早就* 運行結束了,b()所在的上下文環境已經消失了。
* 若是b()或c()報錯,錯誤堆棧將不包括a()。
*/
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, 200)
    });
}
function c() {
    throw Error(10);
}
const a = () => {
    b().then(() => c());
};
a();
/**
* 改爲async函數
*/
const m = async () => {
    await b();
    c();
};
m();
複製代碼

報錯信息以下,能夠看出 async 函數能夠保留運行堆棧。


若是你有更好的答案或想法,歡迎在這題目對應的github下留言:使用 async/await 須要注意什麼?

4.如何實現 Promise.race?

在代碼實現前,咱們須要先了解 Promise.race 的特色:

  1. Promise.race返回的仍然是一個Promise. 它的狀態與第一個完成的Promise的狀態相同。它能夠是完成( resolves),也能夠是失敗(rejects),這要取決於第一個Promise是哪種狀態。

  2. 若是傳入的參數是不可迭代的,那麼將會拋出錯誤。

  3. 若是傳的參數數組是空,那麼返回的 promise 將永遠等待。

  4. 若是迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析爲迭代中找到的第一個值。

Promise.race = function (promises) {
    //promises 必須是一個可遍歷的數據結構,不然拋錯
    return new Promise((resolve, reject) => {
        if (typeof promises[Symbol.iterator] !== 'function') {
            //真實不是這個錯誤
            Promise.reject('args is not iteratable!');
        }
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}複製代碼

測試代碼:

//一直在等待態
Promise.race([]).then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
//拋錯
Promise.race().then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
Promise.race([
    new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
    new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
    new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
});複製代碼

引伸: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的實現原理,若是還不太會,戳:Promise源碼實現

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:如何實現 Promise.race?


5.可遍歷數據結構的有什麼特色?

一個對象若是要具有可被 for...of 循環調用的 Iterator 接口,就必須在其 Symbol.iterator 的屬性上部署遍歷器生成方法(或者原型鏈上的對象具備該方法)

PS: 遍歷器對象根本特徵就是具備next方法。每次調用next方法,都會返回一個表明當前成員的信息對象,具備value和done兩個屬性。

//如爲對象添加Iterator 接口;
let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    [Symbol.iterator]() {
        const self = this;
        const keys = Object.keys(self);
        let index = 0;
        return {
            next() {
                if (index < keys.length) {
                    return {
                        value: self[keys[index++]],
                        done: false
                    };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

for(let item of obj) {
    console.log(item); //Yvette  18  engineer
}複製代碼

使用 Generator 函數(遍歷器對象生成函數)簡寫 Symbol.iterator 方法,能夠簡寫以下:

let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    * [Symbol.iterator] () {
        const self = this;
        const keys = Object.keys(self);
        for (let index = 0;index < keys.length; index++) {
            yield self[keys[index]];//yield表達式僅能使用在 Generator 函數中
        } 
    }
};複製代碼

原生具有 Iterator 接口的數據結構以下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象
  • ES6 的數組、Set、Map 都部署瞭如下三個方法: entries() / keys() / values(),調用後都返回遍歷器對象。

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:可遍歷數據結構的有什麼特色?

6.requestAnimationFrame 和 setTimeout/setInterval 有什麼區別?使用 requestAnimationFrame 有哪些好處?

在 requestAnimationFrame 以前,咱們主要使用 setTimeout/setInterval 來編寫JS動畫。

編寫動畫的關鍵是循環間隔的設置,一方面,循環間隔足夠短,動畫效果才能顯得平滑流暢;另外一方面,循環間隔還要足夠長,才能確保瀏覽器有能力渲染產生的變化。

大部分的電腦顯示器的刷新頻率是60HZ,也就是每秒鐘重繪60次。大多數瀏覽器都會對重繪操做加以限制,不超過顯示器的重繪頻率,由於即便超過那個頻率用戶體驗也不會提高。所以,最平滑動畫的最佳循環間隔是 1000ms / 60 ,約爲16.7ms。

setTimeout/setInterval 有一個顯著的缺陷在於時間是不精確的,setTimeout/setInterval 只能保證延時或間隔不小於設定的時間。由於它們實際上只是把任務添加到了任務隊列中,可是若是前面的任務尚未執行完成,它們必需要等待。

requestAnimationFrame 纔有的是系統時間間隔,保持最佳繪製效率,不會由於間隔時間太短,形成過分繪製,增長開銷;也不會由於間隔時間太長,使用動畫卡頓不流暢,讓各類網頁動畫效果可以有一個統一的刷新機制,從而節省系統資源,提升系統性能,改善視覺效果。

綜上所述,requestAnimationFrame 和 setTimeout/setInterval 在編寫動畫時相比,優勢以下:

1.requestAnimationFrame 不須要設置時間,採用系統時間間隔,能達到最佳的動畫效果。

2.requestAnimationFrame 會把每一幀中的全部DOM操做集中起來,在一次重繪或迴流中就完成。

3.當 requestAnimationFrame() 運行在後臺標籤頁或者隱藏的 <iframe> 裏時,requestAnimationFrame() 會被暫停調用以提高性能和電池壽命(大多數瀏覽器中)。

requestAnimationFrame 使用(試試使用requestAnimationFrame寫一個移動的小球,從A移動到B初):

function step(timestamp) {
    //code...
    window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什麼區別?使用 requestAnimationFrame 有哪些好處?

7.JS 類型轉換的規則是什麼?

類型轉換的規則三言兩語說不清,真想哇得一聲哭出來~


JS中類型轉換分爲 強制類型轉換 和 隱式類型轉換 。

  • 經過 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),進行強制類型轉換。

  • 邏輯運算符(&&、 ||、 !)、運算符(+、-、*、/)、關係操做符(>、 <、 <= 、>=)、相等運算符(==)或者 if/while 的條件,可能會進行隱式類型轉換。

強制類型轉換

1.Number() 將任意類型的參數轉換爲數值類型

規則以下:

  • 若是是布爾值,true和false分別被轉換爲1和0
  • 若是是數字,返回自身
  • 若是是 null,返回 0
  • 若是是 undefined,返回 NAN
  • 若是是字符串,遵循如下規則:
    1. 若是字符串中只包含數字(或者是 0X / 0x 開頭的十六進制數字字符串,容許包含正負號),則將其轉換爲十進制
    2. 若是字符串中包含有效的浮點格式,將其轉換爲浮點數值
    3. 若是是空字符串,將其轉換爲0
    4. 如不是以上格式的字符串,均返回 NaN
  • 若是是Symbol,拋出錯誤
  • 若是是對象,則調用對象的 valueOf() 方法,而後依據前面的規則轉換返回的值。若是轉換的結果是 NaN ,則調用對象的 toString() 方法,再次依照前面的規則轉換返回的字符串值。

部份內置對象調用默認的 valueOf 的行爲:

對象 返回值
Array 數組自己(對象類型)
Boolean 布爾值(原始類型)
Date 從 UTC 1970 年 1 月 1 日午夜開始計算,到所封裝的日期所通過的毫秒數
Function 函數自己(對象類型)
Number 數字值(原始類型)
Object 對象自己(對象類型)
String 字符串值(原始類型)
Number('0111'); //111
Number('0X11') //17
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11);//-17複製代碼

2.parseInt(param, radix)

若是第一個參數傳入的是字符串類型:

  1. 忽略字符串前面的空格,直至找到第一個非空字符,若是是空字符串,返回NaN
  2. 若是第一個字符不是數字符號或者正負號,返回NaN
  3. 若是第一個字符是數字/正負號,則繼續解析直至字符串解析完畢或者遇到一個非數字符號爲止

若是第一個參數傳入的Number類型:

  1. 數字若是是0開頭,則將其看成八進制來解析(若是是一個八進制數);若是以0x開頭,則將其看成十六進制來解析

若是第一個參數是 null 或者是 undefined,或者是一個對象類型:

  1. 返回 NaN

若是第一個參數是數組: 1. 去數組的第一個元素,按照上面的規則進行解析

若是第一個參數是Symbol類型: 1. 拋出錯誤

若是指定radix參數,以radix爲基數進行解析

parseInt('0111'); //111
parseInt(0111); //八進制數 73
parseInt('');//NaN
parseInt('0X11'); //17
parseInt('1a') //1
parseInt('a1'); //NaN
parseInt(['10aa','aaa']);//10

parseInt([]);//NaN; parseInt(undefined);複製代碼

parseFloat

規則和parseInt基本相同,接受一個Number類型或字符串,若是是字符串中,那麼只有第一個小數點是有效的。

toString()

規則以下:

  • 若是是Number類型,輸出數字字符串
  • 若是是 null 或者是 undefined,拋錯
  • 若是是數組,那麼將數組展開輸出。空數組,返回''
  • 若是是對象,返回 [object Object]
  • 若是是Date, 返回日期的文字表示法
  • 若是是函數,輸出對應的字符串(以下demo)
  • 若是是Symbol,輸出Symbol字符串
let arry = [];
let obj = {a:1};
let sym = Symbol(100);
let date = new Date();
let fn = function() {console.log('穩住,咱們能贏!')}
let str = 'hello world';
console.log([].toString()); // ''
console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6
console.log(arry.toString()); // 1,2,3
console.log(obj.toString()); // [object Object]
console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST)
console.log(fn.toString());// function () {console.log('穩住,咱們能贏!')}
console.log(str.toString());// 'hello world'
console.log(sym.toString());// Symbol(100)
console.log(undefined.toString());// 拋錯
console.log(null.toString());// 拋錯複製代碼

String()

String() 的轉換規則與 toString() 基本一致,最大的一點不一樣在於 nullundefined,使用 String 進行轉換,null 和 undefined對應的是字符串 'null''undefined'

Boolean

除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 轉換出來是false,其它都是true.

隱式類型轉換

&& 、|| 、 ! 、 if/while 的條件判斷

須要將數據轉換成 Boolean 類型,轉換規則同 Boolean 強制類型轉換

運算符: + - * /

+ 號操做符,不只能夠用做數字相加,還能夠用做字符串拼接。

僅當 + 號兩邊都是數字時,進行的是加法運算。若是兩邊都是字符串,直接拼接,無需進行隱式類型轉換。

除了上面的狀況外,若是操做數是對象、數值或者布爾值,則調用toString()方法取得字符串值(toString轉換規則)。對於 undefined 和 null,分別調用String()顯式轉換爲字符串,而後再進行拼接。

console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610複製代碼

-*/ 操做符針對的是運算,若是操做值之一不是數值,則被隱式調用Number()函數進行轉換。若是其中有一個轉換除了爲NaN,結果爲NaN.

關係操做符: ==、>、< 、<=、>=

> , <<=>=

  1. 若是兩個操做值都是數值,則進行數值比較
  2. 若是兩個操做值都是字符串,則比較字符串對應的字符編碼值
  3. 若是有一方是Symbol類型,拋出錯誤
  4. 除了上述狀況以外,都進行Number()進行類型轉換,而後再進行比較。

注: NaN是很是特殊的值,它不和任何類型的值相等,包括它本身,同時它與任何類型的值比較大小時都返回false。

console.log(10 > {});//返回false.
/**
 *{}.valueOf ---> {}
 *{}.toString() ---> '[object Object]' ---> NaN
 *NaN 和 任何類型比大小,都返回 false
 */複製代碼

相等操做符:==

  1. 若是類型相同,無需進行類型轉換。
  2. 若是其中一個操做值是 null 或者是 undefined,那麼另外一個操做符必須爲 null 或者 undefined 時,才返回 true,不然都返回 false.
  3. 若是其中一個是 Symbol 類型,那麼返回 false.
  4. 兩個操做值是否爲 string 和 number,就會將字符串轉換爲 number
  5. 若是一個操做值是 boolean,那麼轉換成 number
  6. 若是一個操做值爲 object 且另外一方爲 string、number 或者 symbol,是的話就會把 object 轉爲原始類型再進行判斷(調用object的valueOf/toString方法進行轉換)

對象如何轉換成原始數據類型

若是部署了 [Symbol.toPrimitive] 接口,那麼調用此接口,若返回的不是基礎數據類型,跑出錯誤。

若是沒有部署 [Symbol.toPrimitive] 接口,那麼先返回 valueOf() 的值,若返回的不是基礎類型的值,再返回 toString() 的值,若返回的不是基礎類型的值, 則拋出異常。

//先調用 valueOf, 後調用 toString
let obj = {
    [Symbol.toPrimitive]() {
        return 200;
    },
    valueOf() {
        return 300;
    },
    toString() {
        return 'Hello';
    }
}
//若是 valueOf 返回的不是基本數據類型,則會調用 toString, 
//若是 toString 返回的也不是基本數據類型,會拋出錯誤
console.log(obj + 200); //400複製代碼


若是你有更好的答案或想法,歡迎在這題目對應的github下留言:JS 類型轉換的規則是什麼?

8.簡述下對 webWorker 的理解?

HTML5則提出了 Web Worker 標準,表示js容許多線程,可是子線程徹底受主線程控制而且不能操做dom,只有主線程能夠操做dom,因此js本質上依然是單線程語言。

web worker就是在js單線程執行的基礎上開啓一個子線程,進行程序處理,而不影響主線程的執行,當子線程執行完以後再回到主線程上,在這個過程當中不影響主線程的執行。子線程與主線程之間提供了數據交互的接口postMessage和onmessage,來進行數據發送和接收。

var worker = new Worker('./worker.js'); //建立一個子線程
worker.postMessage('Hello');
worker.onmessage = function (e) {
    console.log(e.data); //Hi
    worker.terminate(); //結束線程
};複製代碼
//worker.js
onmessage = function (e) {
    console.log(e.data); //Hello
    postMessage("Hi"); //向主進程發送消息
};複製代碼

僅是最簡示例代碼,項目中一般是將一些耗時較長的代碼,放在子線程中運行。

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:簡述下對 webWorker 的理解

9.ES6模塊和CommonJS模塊的差別?

  1. ES6模塊在編譯時,就能肯定模塊的依賴關係,以及輸入和輸出的變量。

    CommonJS 模塊,運行時加載。

  2. ES6 模塊自動採用嚴格模式,不管模塊頭部是否寫了 "use strict"; (嚴格模式有哪些限制?[//連接])

  3. require 能夠作動態加載,import 語句作不到,import 語句必須位於頂層做用域中。

  4. ES6 模塊中頂層的 this 指向 undefined,ommonJS 模塊的頂層 this 指向當前模塊。

  5. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。如:

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
    name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William複製代碼

對比 ES6 模塊看一下:

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:ES6模塊和CommonJS模塊的差別?

10.瀏覽器事件代理機制的原理是什麼?

在說瀏覽器事件代理機制原理以前,咱們首先了解一下事件流的概念,早期瀏覽器,IE採用的是事件捕獲事件流,而Netscape採用的則是事件捕獲。"DOM2級事件"把事件流分爲三個階段,捕獲階段、目標階段、冒泡階段。現代瀏覽器也都遵循此規範。


那麼事件代理是什麼呢?

事件代理又稱爲事件委託,在祖先級DOM元素綁定一個事件,當觸發子孫級DOM元素的事件時,利用事件冒泡的原理來觸發綁定在祖先級DOM的事件。由於事件會從目標元素一層層冒泡至document對象。

爲何要事件代理?

  1. 添加到頁面上的事件數量會影響頁面的運行性能,若是添加的事件過多,會致使網頁的性能降低。採用事件代理的方式,能夠大大減小注冊事件的個數。

  2. 事件代理的當時,某個子孫元素是動態增長的,不須要再次對其進行事件綁定。

  3. 不用擔憂某個註冊了事件的DOM元素被移除後,可能沒法回收其事件處理程序,咱們只要把事件處理程序委託給更高層級的元素,就能夠避免此問題。

如將頁面中的全部click事件都代理到document上:

addEventListener 接受3個參數,分別是要處理的事件名、處理事件程序的函數和一個布爾值。布爾值默認爲false。表示冒泡階段調用事件處理程序,若設置爲true,表示在捕獲階段調用事件處理程序。

document.addEventListener('click', function (e) {
    console.log(e.target);
    /**
    * 捕獲階段調用調用事件處理程序,eventPhase是 1; 
    * 處於目標,eventPhase是2 
    * 冒泡階段調用事件處理程序,eventPhase是 1;
    */ 
    console.log(e.eventPhase);
    
});複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:瀏覽器事件代理機制的原理是什麼?


11.js如何自定義事件?

自定義 DOM 事件(不考慮IE9以前版本)

自定義事件有三種方法,一種是使用 new Event(), 另外一種是 createEvent('CustomEvent') , 另外一種是 new customEvent()

  1. 使用 new Event()

獲取不到 event.detail

let btn = document.querySelector('#btn');
let ev = new Event('alert', {
    bubbles: true,    //事件是否冒泡;默認值false
    cancelable: true, //事件可否被取消;默認值false
    composed: false
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable); //true
    console.log(event.detail); //undefined
}, false);
btn.dispatchEvent(ev);複製代碼
  1. 使用 createEvent('CustomEvent') (DOM3)

要建立自定義事件,能夠調用 createEvent('CustomEvent'),返回的對象有 initCustomEvent 方法,接受如下四個參數:

  • type: 字符串,表示觸發的事件類型,如此處的'alert'
  • bubbles: 布爾值: 表示事件是否冒泡
  • cancelable: 布爾值,表示事件是否能夠取消
  • detail: 任意值,保存在 event 對象的 detail 屬性中
let btn = document.querySelector('#btn');
let ev = btn.createEvent('CustomEvent');
ev.initCustomEvent('alert', true, true, 'button');
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
複製代碼複製代碼
  1. 使用 new customEvent() (DOM4)

使用起來比 createEvent('CustomEvent') 更加方便

var btn = document.querySelector('#btn');
/*
 * 第一個參數是事件類型
 * 第二個參數是一個對象
 */
var ev = new CustomEvent('alert', {
    bubbles: 'true',
    cancelable: 'true',
    detail: 'button'
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);複製代碼

自定義非 DOM 事件(觀察者模式)

EventTarget類型有一個單獨的屬性handlers,用於存儲事件處理程序(觀察者)。

addHandler() 用於註冊給定類型事件的事件處理程序;

fire() 用於觸發一個事件;

removeHandler() 用於註銷某個事件類型的事件處理程序。

function EventTarget(){
    this.handlers = {};
}

EventTarget.prototype = {
    constructor:EventTarget,
    addHandler:function(type,handler){
        if(typeof this.handlers[type] === "undefined"){
            this.handlers[type] = [];
        }
        this.handlers[type].push(handler);
    },
    fire:function(event){
        if(!event.target){
            event.target = this;
        }
        if(this.handlers[event.type] instanceof Array){
            const handlers = this.handlers[event.type];
            handlers.forEach((handler)=>{
                handler(event);
            });
        }
    },
    removeHandler:function(type,handler){
        if(this.handlers[type] instanceof Array){
            const handlers = this.handlers[type];
            for(var i = 0,len = handlers.length; i < len; i++){
                if(handlers[i] === handler){
                    break;
                }
            }
            handlers.splice(i,1);
        }
    }
}
//使用
function handleMessage(event){
    console.log(event.message);
}
//建立一個新對象
var target = new EventTarget();
//添加一個事件處理程序
target.addHandler("message", handleMessage);
//觸發事件
target.fire({type:"message", message:"Hi"}); //Hi
//刪除事件處理程序
target.removeHandler("message",handleMessage);
//再次觸發事件,沒有事件處理程序
target.fire({type:"message",message: "Hi"});複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:js如何自定義事件?

12.跨域的方法有哪些?原理是什麼?

知其然知其因此然,在說跨域方法以前,咱們先了解下什麼叫跨域,瀏覽器有同源策略,只有當「協議」、「域名」、「端口號」都相同時,才能稱之爲是同源,其中有一個不一樣,便是跨域。

那麼同源策略的做用是什麼呢?同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。

那麼咱們又爲何須要跨域呢?一是前端和服務器分開部署,接口請求須要跨域,二是咱們可能會加載其它網站的頁面做爲iframe內嵌。

跨域的方法有哪些?

經常使用的跨域方法

  1. jsonp

儘管瀏覽器有同源策略,可是 <script> 標籤的 src 屬性不會被同源策略所約束,能夠獲取任意服務器上的腳本並執行。jsonp 經過插入script標籤的方式來實現跨域,參數只能經過url傳入,僅能支持get請求。

實現原理:

Step1: 建立 callback 方法

Step2: 插入 script 標籤

Step3: 後臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的調用,而且數據做爲參數傳入該方法

Step4: 前端執行服務端返回的方法調用

下面代碼僅爲說明 jsonp 原理,項目中請使用成熟的庫。分別看一下前端和服務端的簡單實現:

//前端代碼
function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //建立script標籤
        let script = document.createElement('script');
        //將回調函數掛在 window 上
        window[cb] = function(data) {
            resolve(data);
            //代碼執行後,刪除插入的script標籤
            document.body.removeChild(script);
        }
        //回調函數加在請求地址上
        params = {...params, cb} //wb=b&cb=show
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}
//使用
function sayHi(data) {
    console.log(data);
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        //code
    },
    cb: 'sayHi'
}).then(data => {
    console.log(data);
});複製代碼
//express啓動一個後臺服務
let express = require('express');
let app = express();

app.get('/say', (req, res) => {
    let {cb} = req.query; //獲取傳來的callback函數名,cb是key
    res.send(`${cb}('Hello!')`);
});
app.listen(3000);複製代碼

從今天起,jsonp的原理就要了然於心啦~


  1. cors

jsonp 只能支持 get 請求,cors 能夠支持多種請求。cors 並不須要前端作什麼工做。

簡單跨域請求:

只要服務器設置的Access-Control-Allow-Origin Header和請求來源匹配,瀏覽器就容許跨域

  1. 請求的方法是get,head或者post。
  2. Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一個值,或者不設置也能夠,通常默認就是application/x-www-form-urlencoded。
  3. 請求中沒有自定義的HTTP頭部,如x-token。(應該是這幾種頭部 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type)
//簡單跨域請求
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXXX');
});複製代碼

帶預檢(Preflighted)的跨域請求

不滿於簡單跨域請求的,便是帶預檢的跨域請求。服務端須要設置 Access-Control-Allow-Origin (容許跨域資源請求的域) 、 Access-Control-Allow-Methods (容許的請求方法) 和 Access-Control-Allow-Headers (容許的請求頭)

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXX');
    res.setHeader('Access-Control-Allow-Headers', 'XXX'); //容許返回的頭
    res.setHeader('Access-Control-Allow-Methods', 'XXX');//容許使用put方法請求接口
    res.setHeader('Access-Control-Max-Age', 6); //預檢的存活時間
    if(req.method === "OPTIONS") {
        res.end(); //若是method是OPTIONS,不作處理
    }
});複製代碼

更多CORS的知識能夠訪問: HTTP訪問控制(CORS)

  1. nginx 反向代理

使用nginx反向代理實現跨域,只須要修改nginx的配置便可解決跨域問題。

A網站向B網站請求某個接口時,向B網站發送一個請求,nginx根據配置文件接收這個請求,代替A網站向B網站來請求。 nginx拿到這個資源後再返回給A網站,以此來解決了跨域問題。

例如nginx的端口號爲 8090,須要請求的服務器端口號爲 3000。(localhost:8090 請求 localhost:3000/say)

nginx配置以下:

server {
    listen       8090;

    server_name  localhost;

    location / {
        root   /Users/liuyan35/Test/Study/CORS/1-jsonp;
        index  index.html index.htm;
    }
    location /say {
        rewrite  ^/say/(.*)$ /$1 break;
        proxy_pass   http://localhost:3000;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    }
    # others
}複製代碼
  1. websocket

Websocket 是 HTML5 的一個持久化的協議,它實現了瀏覽器與服務器的全雙工通訊,同時也是跨域的一種解決方案。

Websocket 不受同源策略影響,只要服務器端支持,無需任何配置就支持跨域。

前端頁面在 8080 的端口。

let socket = new WebSocket('ws://localhost:3000'); //協議是ws
socket.onopen = function() {
    socket.send('Hi,你好');
}
socket.onmessage = function(e) {
    console.log(e.data)
}複製代碼

服務端 3000端口。能夠看出websocket無需作跨域配置。

let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3000});
wss.on('connection', function(ws) {
    ws.on('message', function(data) {
        console.log(data); //接受到頁面發來的消息'Hi,你好'
        ws.send('Hi'); //向頁面發送消息
    });
});複製代碼
  1. postMessage

postMessage 經過用做前端頁面以前的跨域,如父頁面與iframe頁面的跨域。window.postMessage方法,容許跨窗口通訊,不論這兩個窗口是否同源。

話說工做中兩個頁面以前須要通訊的狀況並很少,我本人工做中,僅使用過兩次,一次是H5頁面中發送postMessage信息,ReactNative的webview中接收此此消息,並做出相應處理。另外一次是可輪播的頁面,某個輪播頁使用的是iframe頁面,爲了解決滑動的事件衝突,iframe頁面中去監聽手勢,發送消息告訴父頁面是否左滑和右滑。

子頁面向父頁面發消息

父頁面

window.addEventListener('message', (e) => {
    this.props.movePage(e.data);
}, false);複製代碼

子頁面(iframe):

if(/*左滑*/) {
    window.parent && window.parent.postMessage(-1, '*')
}else if(/*右滑*/){
    window.parent && window.parent.postMessage(1, '*')
}複製代碼

父頁面向子頁面發消息

父頁面:

let iframe = document.querySelector('#iframe');
iframe.onload = function() {
    iframe.contentWindow.postMessage('hello', 'http://localhost:3002');
}複製代碼

子頁面:

window.addEventListener('message', function(e) {
    console.log(e.data);
    e.source.postMessage('Hi', e.origin); //回消息
});複製代碼
  1. node 中間件

node 中間件的跨域原理和nginx代理跨域,同源策略是瀏覽器的限制,服務端沒有同源策略。

node中間件實現跨域的原理以下:

1.接受客戶端請求

2.將請求 轉發給服務器。

3.拿到服務器 響應 數據。

4.將 響應 轉發給客戶端。

不經常使用跨域方法

如下三種跨域方式不多用,若有興趣,可自行查閱相關資料。

  1. window.name + iframe

  2. location.hash + iframe

  3. document.domain (主域需相同)

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:跨域的方法有哪些?原理是什麼?

13.js異步加載的方式有哪些?

  1. <script> 的 defer 屬性,HTML4 中新增

  2. <script> 的 async 屬性,HTML5 中新增

<script>標籤打開defer屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。

defer 和 async 的區別在於: defer要等到整個頁面在內存中正常渲染結束,纔會執行;

async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。defer是「渲染完再執行」,async是「下載完就執行」。

若是有多個 defer 腳本,會按照它們在頁面出現的順序加載。

多個async腳本是不能保證加載順序的。

  1. 動態插入 script 腳本
function downloadJS() { 
    varelement = document.createElement("script"); 
    element.src = "XXX.js"; 
    document.body.appendChild(element); 
}
//什麼時候的時候,調用上述方法 複製代碼
  1. 有條件的動態建立腳本

如頁面 onload 以後,

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:js異步加載的方式有哪些?

14.下面代碼a在什麼狀況中打印出1?

//?
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}
複製代碼複製代碼

1.在類型轉換的時候,咱們知道了對象如何轉換成原始數據類型。若是部署了 [Symbol.toPrimitive],那麼返回的就是Symbol.toPrimitive的返回值。固然,咱們也能夠把此函數部署在valueOf或者是toString接口上,效果相同。

//利用閉包延長做用域的特性
let a = {
    [Symbol.toPrimitive]: (function() {
            let i = 1;
            return function() {
                return i++;
            }
    })()
}複製代碼

(1). 比較 a == 1 時,會調用 [Symbol.toPrimitive],此時 i 是 1,相等。 (2). 繼續比較 a == 2,調用 [Symbol.toPrimitive],此時 i 是 2,相等。 (3). 繼續比較 a == 3,調用 [Symbol.toPrimitive],此時 i 是 3,相等。

2.利用Object.definePropert在window/global上定義a屬性,獲取a屬性時,會調用get.

let val = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return val++;
  }
});
複製代碼複製代碼

3.利用數組的特性。

var a = [1,2,3];
a.join = a.shift;複製代碼

數組的 toString 方法返回一個字符串,該字符串由數組中的每一個元素的 toString() 返回值經調用 join() 方法鏈接(由逗號隔開)組成。

所以,咱們能夠從新 join 方法。返回第一個元素,並將其刪除。

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:下面代碼a在什麼狀況中打印出1?

15.下面這段代碼的輸出是什麼?

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();複製代碼

**說明:**一道經典的面試題,僅是爲了幫助你們回顧一下知識點,加深理解,真實工做中,是不可能這樣寫代碼的,不然,確定會被打死的。

1.首先預編譯階段,變量聲明與函數聲明提高至其對應做用域的最頂端。

所以上面的代碼編譯後以下(函數聲明的優先級先於變量聲明):

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
var getName;
function getName() {console.log(5)};
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
getName = function() {console.log(4)};複製代碼

2.Foo.getName();直接調用Foo上getName方法,輸出2

3.getName();輸出4,getName被從新賦值了

4.Foo().getName();執行Foo(),window的getName被從新賦值,返回this;瀏覽器環境中,非嚴格模式,this 指向 window,this.getName();輸出爲1.

若是是嚴格模式,this 指向 undefined,此處會拋出錯誤。

若是是node環境中,this 指向 global,node的全局變量並不掛在global上,由於global.getName對應的是undefined,不是一個function,會拋出錯誤。

5.getName();已經拋錯的天然走不動這一步了;繼續瀏覽器非嚴格模式;window.getName被從新賦過值,此時再調用,輸出的是1

6.new Foo.getName();考察運算符優先級的知識,new 無參數列表,對應的優先級是18;成員訪問操做符 . , 對應的優先級是 19。所以至關因而 new (Foo.getName)();new操做符會執行構造函數中的方法,所以此處輸出爲 2.

7.new Foo().getName();new 帶參數列表,對應的優先級是19,和成員訪問操做符.優先級相同。同級運算符,按照從左到右的順序依次計算。new Foo()先初始化 Foo 的實例化對象,實例上沒有getName方法,所以須要原型上去找,即找到了 Foo.prototype.getName,輸出3

8.new new Foo().getName(); new 帶參數列表,優先級19,所以至關因而 new (new Foo()).getName();先初始化 Foo 的實例化對象,而後將其原型上的 getName 函數做爲構造函數再次 new ,輸出3

所以最終結果以下:

Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:下面這段代碼的輸出是什麼?

16.實現雙向綁定 Proxy 與 Object.defineProperty 相比優劣如何?

  1. Object.definedProperty 的做用是劫持一個對象的屬性,劫持屬性的getter和setter方法,在對象的屬性發生變化時進行特定的操做。而 Proxy 劫持的是整個對象。

  2. Proxy 會返回一個代理對象,咱們只須要操做新對象便可,而 Object.defineProperty 只能遍歷對象屬性直接修改。

  3. Object.definedProperty 不支持數組,更準確的說是不支持數組的各類API,由於若是僅僅考慮arry[i] = value 這種狀況,是能夠劫持的,可是這種劫持意義不大。而 Proxy 能夠支持數組的各類API。

  4. 儘管 Object.defineProperty 有諸多缺陷,可是其兼容性要好於 Proxy.

PS: Vue2.x 使用 Object.defineProperty 實現數據雙向綁定,V3.0 則使用了 Proxy.

//攔截器
let obj = {};
let temp = 'Yvette';
Object.defineProperty(obj, 'name', {
    get() {
        console.log("讀取成功");
        return temp
    },
    set(value) {
        console.log("設置成功");
        temp = value;
    }
});

obj.name = 'Chris';
console.log(obj.name);複製代碼

PS: Object.defineProperty 定義出來的屬性,默認是不可枚舉,不可更改,不可配置【沒法delete】

咱們能夠看到 Proxy 會劫持整個對象,讀取對象中的屬性或者是修改屬性值,那麼就會被劫持。可是有點須要注意,複雜數據類型,監控的是引用地址,而不是值,若是引用地址沒有改變,那麼不會觸發set。

let obj = {name: 'Yvette', hobbits: ['travel', 'reading'], info: {
    age: 20,
    job: 'engineer'
}};
let p = new Proxy(obj, {
    get(target, key) { //第三個參數是 proxy, 通常不使用
        console.log('讀取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        if(key === 'length') return true; //若是是數組長度的變化,返回。
        console.log('設置成功');
        return Reflect.set([target, key, value]);
    }
});
p.name = 20; //設置成功
p.age = 20; //設置成功; 不須要事先定義此屬性
p.hobbits.push('photography'); //讀取成功;注意不會觸發設置成功
p.info.age = 18; //讀取成功;不會觸發設置成功複製代碼

最後,咱們再看下對於數組的劫持,Object.definedProperty 和 Proxy 的差異

Object.definedProperty 能夠將數組的索引做爲屬性進行劫持,可是僅支持直接對 arry[i] 進行操做,不支持數組的API,很是雞肋。

let arry = []
Object.defineProperty(arry, '0', {
    get() {
        console.log("讀取成功");
        return temp
    },
    set(value) {
        console.log("設置成功");
        temp = value;
    }
});

arry[0] = 10; //觸發設置成功
arry.push(10); //不能被劫持複製代碼

Proxy 能夠監聽到數組的變化,支持各類API。注意數組的變化觸發get和set可能不止一次,若有須要,自行根據key值決定是否要進行處理。

let hobbits = ['travel', 'reading'];
let p = new Proxy(hobbits, {
    get(target, key) {
        // if(key === 'length') return true; //若是是數組長度的變化,返回。
        console.log('讀取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        // if(key === 'length') return true; //若是是數組長度的變化,返回。
        console.log('設置成功');
        return Reflect.set([target, key, value]);
    }
});
p.splice(0,1) //觸發get和set,能夠被劫持
p.push('photography');//觸發get和set
p.slice(1); //觸發get;由於 slice 是不會修改原數組的複製代碼

若是你有更好的答案或想法,歡迎在這題目對應的github下留言:實現雙向綁定 Proxy 與 Object.defineProperty 相比優劣如何?

17.Object.is() 與比較操做符 ===、== 有什麼區別?

如下狀況,Object.is認爲是相等

兩個值都是 undefined
兩個值都是 null
兩個值都是 true 或者都是 false
兩個值是由相同個數的字符按照相同的順序組成的字符串
兩個值指向同一個對象
兩個值都是數字而且
都是正零 +0
都是負零 -0
都是 NaN
都是除零和 NaN 外的其它同一個數字複製代碼

Object.is() 相似於 ===,可是有一些細微差異,以下:

  1. NaN 和 NaN 相等
  2. -0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true
console.log(NaN === NaN);//false
console.log(Object.is(-0, +0)); //false
console.log(-0 === +0); //true複製代碼

Object.is 和 ==差得遠了, == 在類型不一樣時,須要進行類型轉換,前文已經詳細說明。

本文轉載自 寒冬求職季之你必需要懂的原生JS

相關文章
相關標籤/搜索