重整旗鼓,2019自結前端面試小冊【JavaScript】

前言

2020年已經到來,是否是該爲了更好的2020年再戰一回呢? ‘勝敗兵家事不期,包羞忍恥是男兒。江東子弟多才俊,捲土重來未可知’,那些在秋招失利的人,難道就心甘情願放棄嗎!javascript

此文總結2019年以來本人經歷以及瀏覽文章中,較熱門的一些面試題,涵蓋從CSS到JS再到Vue再到網絡等前端基礎到進階的一些知識。css

總結面試題涉及的知識點是對本身的一個提高,也但願能夠幫助到同窗們,在2020年會有一個更好的競爭能力。前端

Module Two - JavaScript

1 - 基本類型

  • 簡單數據類型(原始類型): String、Number、Boolean、Null、Undefined、Symbol
  • 複雜數據類型(對象類型): Object

Null是對象嗎?java

雖然typeof null返回的是object,但這實際上是JavaScript長久以來遺留的一個bugnull實質上是基本數據類型的一種node

簡單數據類型與複雜數據類型在數據存儲上有什麼區別?jquery

簡單數據類型以的形式存儲,存儲的是es6

複雜數據類型以的形式存儲,地址(指向堆中的值)存儲在中。面試

❗ 小知識: 當咱們把對象賦給另一個變量時,複製的是地址,指向同一塊內存空間,因此當其中一個對象改變時,另一個對象也會隨之改變ajax

棧內存與對堆內存的區別

  • 棧內存
JavaScript中原始類型的值被直接存儲在棧中,在定義變量時,棧就爲其分配好內存空間

- 存儲的值大小固定
- 空間較小
- 能夠直接操做其保存的變量,運行效率高
- 由系統自動分配存儲空間
複製代碼
  • 堆內存
JavaScript中引用類型(對象類型)的值實際存儲在堆內存中,
它在棧中只存儲了一個固定長度的地址,這個地址指向堆內存中的值

- 存儲的值大小不定,可動態調整
- 空間較大,運行效率低
- 沒法直接操做其內部存儲,使用其地址讀取值
- 經過代碼分配空間
複製代碼

2 - typeof與instanceof的做用和區別是什麼?

  • typeof
- 可以正確判斷簡單數據類型(原始類型),除了null,typeof null結果爲object

- 對於對象而言,typeof不能正確判斷對象類型,typeof僅能夠區分開function,除此以外,結果均爲object
複製代碼
  • instanceof
- 可以準確判斷複雜數據類型,可是不能正確判斷簡單數據類型
複製代碼

instanceof的原理正則表達式

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

❗ 小知識:

var str = 'hello world'  str instanceof String → false

var str = new String('hello world')  str instanceof String → true
複製代碼

3 - 函數參數爲對象的狀況

function test(person) { 
    person.age = 26;
	person = {
        name: 'foo',
        age: 30
    }
    return person
}
const p1 = {
        name: 'bar',
        age: 25
}
const p2 = test(p1)
console.log(p1)  // name:bar  age:26
console.log(p2)  // name:foo  age:30
複製代碼

解析 首先函數傳參是按值傳遞的,即傳遞的是對象指針的副本,到函數內部修改參數這一步,p1的值也被修改,可是當咱們從新爲person分配一個對象時,是建立了一個新的地址(指針),也就和p1沒有關係了,因此最終2個變量的值不一樣


4 - 類型轉換

  • 轉Boolean
在條件判斷中,除了undefined、null、''false、0、-0、NaN,其餘全部值都轉爲true,包括空數據和對象
複製代碼
  • 對象轉原始類型
對象在進行類型轉換時,會調用內部的[[ToPrimitive]]函數

- 若是已是原始類型,則不須要進行轉換
- 調用x.value(),若是轉換爲基礎類型,則返回轉換的值
- 調用x.toString(),若是轉換爲基礎類型,則返回轉換的值
- 若是都不返回原始類型值,則報錯

重寫:
let a = {
    valueOf(){
        // toDo
    },
    toString(){
        // toDo
    },
    [Symbol.toPrimitive](){
        // toDo
    }
}
複製代碼

❗ 小知識:

引用類型 → Number
    先進行valueOf,再進行toString
引用類型 → String
    先進行toString,再進行valueOf

若valueOf和toString都不存在,則返回基本類型錯誤,拋出TypeError

例子:
const Obj1 = {
    valueOf:() => {
        console.log('valueOf')
        return 123
    },
    toString:() => {
        console.log('toString')
        return 'Chicago'
    }
}
const Obj2 = {
    valueOf:() => {
        console.log('valueOf')
        return {}
    },
    toString:() => {
        console.log('toString')
        return '{}'
    }
}
console.log(Obj1 - 1) → valueOf 122
console.log(Obj2 - 1) → valueOf toString TypeError
複製代碼
  • 加法運算
加法運算與其餘有所區別
- 當運算符其中一方爲字符串時,那麼另外一方也轉換爲字符串
- 當一側爲Number類型,另外一側爲原始類型,則將原始類型轉換爲Number
- 當一側爲Number類型,另外一側爲引用類型,則將引用類型和Number類型轉換爲字符串後拼接


例子:
1 + '1''11'
true + true → 2
4 + [1,2,3] → '41,2,3'


Ps: 特別注意 'a'+ +'b',由於 +'b' 會等於NaN,因此結果爲 'aNaN'
複製代碼
  • 除加法外,只要其中一方爲數字,那麼另外一方就會轉換爲數字
  • 比較運算符的轉換規則( == )
- NaN:和其餘類型比較永遠返回false(包括本身)
- Boolean:和其餘類型比較,Boolean首先被轉化爲Number(true → 一、false → 0)
- String和Number:String先轉化爲Number類型,再進行比較
- Null和undefined:null == undefined → true,除此以外,null、undefined和其餘任何值比較均爲false
- 原始類型和引用類型:引用類型轉換爲原始類型
複製代碼

5 - 常見面試點:[] == ![] 爲什麼爲true、[undefined]爲什麼爲false?

1)因爲 ! 的優先級高於 ==![] 首先會被轉換爲false,而後根據Boolean轉換原則,false將會轉換爲Number類型,即轉換爲 0,而後左側的 [] 轉換爲原始類型,也爲 0 ,因此最終結果爲 true

2)數組元素爲null、undefined時,該元素被看成空字符串,因此 [undefined]、[null] 都會變爲 0 , 最終 0 == false → true


6 - 什麼是包裝類型,與原始類型有什麼區別??

包裝類型即 Boolean、Number、String

與原始類型的區別:

true === new Boolean → false
123 === new Number('123') → false
'Chicago' === new String('Chicago') → false

typeof new String('Chicago') → Object
typeof 'Chicago' → string
複製代碼

什麼是裝箱和拆箱? 裝箱即原始類型轉換爲包裝類型、拆箱即包裝類型轉換爲原始類型

如何使用原始類型來調用方法?

原始類型調用方法,實際上自動進行了裝箱和拆箱操做

var name1 = 'Chicago'
var name2 = name1.substring(2)
以上2行代碼,實際上發生了3個事情
- 建立一個String的包裝類實例
- 在實例上調用substring方法
- 銷燬實例
複製代碼

手動裝箱、拆箱

var name = new Number('123')
console.log(typeof name.valueOf()) → number
console.log(typeof name.toString()) → string
複製代碼

7 - 如何讓 a == 1 && a == 2 && a == 3 爲 true?

依據拆箱:
const a = {
    value:[3,2,1],
    valueOf:function(){
        return this.value.pop()
    } // 每次調用,刪除一個元素
}

console.log(a == 1 && a == 2 && a == 3) // true (注意僅能判斷一次)
複製代碼

★ 8 - 如何正確判斷this? 箭頭函數的this又是什麼?

誰調用它,this就指向誰這句話能夠說即精準又帶坑

(綁定方式) 影響this的指向實際有4種:

- 默認綁定:全局調用
- 隱式調用:對象調用
- 顯示調用:call()、apply()、bind()
- new綁定
複製代碼
  • 默認
function foo(){
    console.log(this.a)
}
var a = 2
foo() // 2 → this指向全局
複製代碼
  • 隱式
function foo(){
    console.log(this.a)
}
var obj1 = {
    a = 1,
    foo
}
var obj2 = {
    a = 2,
    foo
}
obj1.foo() // 1 → this 指向 obj1
obj2.foo() // 2 → this 指向 obj2
複製代碼
  • 顯式
function foo(){
    console.log(this.a)
    bar.apply( {a:2},arguments )
}
function bar(b){
    console.log(this.a + b)
}
var a = 1 // 全局 a 變量
foo(3) // 1 5 → 1 說明第一個打印種 this 指向全局,5 說明第二個打印中 this 指向 {a:2}
複製代碼

❗ 小知識:

call()、apply()、bind()三者區別:

call()、apply()屬於當即執行函數,區別在於接收的參數形式不一樣,前者是一次傳入參數,後者參數能夠是數組
bind()則是建立一個新的包裝函數,而且返回,它不會當即執行bind(this,arg1,arg2···)
複製代碼

▲ 當call、apply、bind傳入的第一個參數爲 undefined/null 時,嚴格模式下this值爲傳入的undefined/null,非嚴格模式下,實際應用默認綁定,即指向全局(node環境下指向global、瀏覽器環境下指向window)

function info(){
    console.log(this);
    console.log(this.age);
}
var person = {
	age:20,
	info
}
var age = 28;
var info = person.info;
info.call(null); // window 、 28
複製代碼
  • new綁定
- 構造函數返回值不是function/object
function Super(age){
    this.age = age
}
let instance = new Super('26')
console.log(instance.age) // '26'

- 構造函數返回function/object
function Super(age){
    this.age = age
    let obj = {
        a:2
    }
    return obj
}
let instance = new Super('26')
console.log(instance.age) // undefined → 返回的新obj中沒有age
複製代碼

靈魂拷問:new 的實現原理

1 - 建立一個新對象
2 - 這個新對象會被執行[[原型]]連接
3 - 屬性和方法被加入到this引用的對象裏,並執行構造函數中的方法
4 - 若是函數沒有返回其餘對象,那麼this指向這個新對象,不然this指向構造函數返回的對象
複製代碼

❗ 小知識:

對於this的綁定問題,優先級以下

New > 顯式綁定 > 隱式綁定 > 默認綁定
複製代碼

箭頭函數的this

1) 箭頭函數沒有本身的this

當咱們使用箭頭函數的時候,箭頭函數會默認幫咱們綁定外層this的值,因此在箭頭函數中this的值與外層的this是同樣的

例子1:

const obj = {
    a: () => {
        console.log(this)
    }
}
obj.a()  //打出來的是window

由於箭頭函數默認不會使用本身的this,而是會和外層的this保持一致,最外層的this就是window對象
複製代碼
例子2:

let obj = {
    age:20,
    info:function(){
        return () => {
            console.log(this.age)
        }
    }
}
let person = { age:28 }
let info1 = obj.info()
info1() // 20
let info2 = obj.info.call(person)
info2() // 28
複製代碼

2) 箭頭函數不能在call方法修改裏面的this

函數的this能夠經過call等顯式綁定的方式修改,而爲了減小this的複雜性,箭頭函數沒法用call()來指定this

const obj = {
    a: () => {
        console.log(this)
    }
}
obj.a.call('123')  //打出來的結果依然是window對象
複製代碼

9 - 若是對一個函數進行屢次bind,會出現什麼狀況?

無論咱們給函數進行幾回bind顯式綁定,函數中的this永遠由 第一次bind 決定

let a = {}
let fn = function(){
    console.log(this)
}
fn.bind().bind(a)() // => Window
複製代碼

10 - == 與 === 有什麼區別?

  • == 若是雙方類型不一樣,會自動進行類型轉換
  • === 判斷二者類型與值是否相同,不會進行類型轉換

★ 11 - 何爲閉包?

紅寶書上對於閉包的定義:閉包是指有權訪問另一個函數做用域中的變量的函數

簡單來講,閉包就是一個函數A內部有另外一個函數B,函數B能夠訪問到函數A的變量方法,此時函數B就是閉包

例子:

function A(){
    let a = 1
    window.B = function(){
        console.log(a)
    }
}
A() // 定義a,賦值window.B
B() // 1 → 訪問到函數A內部的變量
複製代碼

閉包存在乎義

在Js中,閉包存在的意義就是讓咱們能夠間接訪問到函數內部的變量 (函數內部變量的引用也會在內部函數中,不會由於執行完函數就被銷燬,但過多的閉包會形成內存泄漏)

閉包的三個特性

  • 閉包能夠訪問當前函數之外的變量
function getOuter(){
    var data = 'outer'
    function getDate(str){
        console.log(str + data) // 訪問外部變量 'data'
    }
    return getDate('I am')
}
getOuter() // I am outer
複製代碼
  • 即便外部函數已經返回,閉包仍能訪問外部函數定義的變量
function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //訪問外部的date
  }
  return getDate;     //外部函數返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"
複製代碼
  • 閉包能夠更新外部變量的值
function updateCount(){
  var count = 0;
  function getCount(val){
    count = val; // 更新外部函數變量
    console.log(count);
  }
  return getCount;     //外部函數返回
}
var count = updateCount();
count(815); //815
count(816); //816
複製代碼

經典面試題:循環中使用閉包解決var定義的問題(循環輸出0-5,結果倒是一堆6?)

for (var i = 0; i <= 5; i++) {
    setTimeout(function () {
      console.log(i)
    }, 1000 * i)
} // 6,6,6,6,6,6

for(var i = 1;i <= 5; i++){
    (function(j){
        setTimeout( () => {
            console.log(j)
        },j * 1000)
    })(i)
} // 0,1,2,3,4,5

// Tips:經過let定義i也可以解決,由於let具備塊級做用域
複製代碼

12 - 淺拷貝?深拷貝?如何實現?

  • 淺拷貝

一、首先能夠經過Object.assign來實現,Object.assgin只會拷貝全部的屬性值到新的對象中,但若是屬性值是一個對象的話,拷貝的是地址,因此並非深拷貝

let obj = {
    a: 1,
    b:{
        foo:'foo',
        bar:'bar'
    }
}
let objCopy = Object.assign({},obj)
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}

obj.a = 2
console.log(objCopy.a) // 1 → 不會隨着obj修改而修改
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'} → 拷貝的是地址,指向同一個值,因此修改obj.b會影響到objCopy
複製代碼

二、也能夠經過展開運算符...來實現淺拷貝

let obj = {
    a: 1
    b:{
        foo:'foo',
        bar:'bar'
    }
}
let objCopy = { ...obj }
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}

obj.a = 2
console.log(objCopy.a) // 1
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'}

與Object.assign同樣,屬性值爲對象的拷貝,拷貝的是地址
複製代碼
  • 深拷貝

一般來講淺拷貝能夠解決大部分的問題,但若是遇到下面這種狀況,就須要深拷貝來解決

let a = {
    age:1,
    jobs:{
        first:'FE'
    }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
複製代碼

淺拷貝只能解決第一層的問題,若是對象的屬性仍是對象的話,該屬性二者會共享相同的地址,假如咱們不想b的對象屬性隨a改變而改變,就須要經過深拷貝

1 - JSON.parse(JSON.stringify(object))

let a = {
    age: 1,
    jobs: { first: 'FE' } 
} 
let b = JSON.parse(JSON.stringify(a)) 
a.jobs.first = 'native' 
console.log(b.jobs.first) // FE
複製代碼
2 - lodash庫中的cloneDeep()
複製代碼

13 - var、let、const的區別是什麼?什麼是變量提高?暫時性死區又是什麼?

三者區別

1 - var【聲明變量】
    var 沒有塊的概念,能夠跨塊訪問,沒法跨函數訪問

2 - let【聲明塊中的變量】
    let 只能在塊做用域裏訪問,不能跨塊訪問,更不能跨函數訪問

3 - const【聲明常量,一旦賦值便不可修改】
    const 只能在塊級做用域裏訪問,並且不能修改值
    
    Tips: 這裏的不能修改,並非變量的值不能改動,而是變量所指向的那個內存地址保存的指針不能改動
    
複製代碼

❗ 小知識:

var a = 1
let b = 1
const c = 1
console.log(window.a) // 1
console.log(window.b) // undefined
console.log(window.c) // undefined

在全局做用域下使用let和const聲明變量,變量並不會被掛載到window上,這一點與var不一樣

關於const,還有兩個注意點:
- const聲明以後必須立刻賦值,不然報錯
- const簡單類型一旦聲明就不能修改,而複雜類型(數組,對象)指針指向的地址不能修改,但內部數據能夠修改
複製代碼

何爲提高?

console.log(a)  // undefined
var a = 1
複製代碼

上面兩行代碼,雖然在打印a前變量並無被聲明,可是卻可使用這個未聲明的變量,不報錯,這一種狀況就叫作提高,並且提高的是聲明

實際上,提高不只僅只能做用於變量的聲明,函數的聲明也會被提高

console.log(a) // f a(){}
function a(){}
var a = 1
複製代碼

函數的聲明優先級高於變量的聲明

何爲暫時性死區?

console.log(a)  // ReferenceError: Cannot access 'a' before initialization
let a
複製代碼

爲什麼此次就會報錯呢? 只要一進入當前做用域,所要使用得變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,才能夠獲取和使用該變量,這就是暫時性死區

var a = 123; // 聲明
if (true) {
  a = 'A'; // 報錯 由於本塊級做用域有a聲明變量
  let a; // 綁定if這個塊級的做用域 不能出現a變量
}
複製代碼

對於暫時性死區,個人理解是聲明提高了,但初始化沒有被提高,而提高是聲明提高,並初始化爲undefined

總結

函數提高優於變量提高,函數提高會把整個函數提高到做用域頂部,變量提高只會把聲明提高到做用域頂部

  • var存在提高,咱們能在聲明以前使用。let和const因爲暫時性死區的緣由,不能在聲明前使用
  • var 在全局做用域下聲明變量會致使變量被掛載到window上,其餘二者不會
  • let / const 做用基本一致,但後者不容許再次賦值
  • let、const不容許在相同做用域內,重複聲明同一個變量

★ 14 - 原型 / 構造函數 / 實例

  • 原型 一個簡單的對象,用於實現對象的屬性繼承
每個JavaScript對象(null除外)在建立的時候就會有一個與之關聯的對象,這個對象就是原型對象

每個對象都會從原型上繼承屬性
複製代碼
  • 構造函數 能夠經過new來建立一個對象的函數
  • 實例 經過構造函數和new建立出來的對象,即是實例
實例經過__proto__指向原型,經過constructor指向構造函數
複製代碼

Object爲例子,Object即是一個構造函數,咱們經過它來構建實例

const instance = new Object()
複製代碼

這裏,instanceObject實例Objectinstance構造函數,構造函數擁有一個prototype的屬性來指向原型,所以有

const prototype = Object.prototype
複製代碼

原型、構造函數、實例 三者關係

實例.__proto__ === 原型
原型.constructor === 構造函數
構造函數.prototype === 原型

Tips:
const instance = new Object()
instance.constructor === Object // true
當獲取 實例.constructor 時,其實實例上並無constructor屬性,當不能讀到constructor屬性時,會從實例的原型中讀取

則有 instance.constructor === instance.__proto__.constructor

若是修改了instance.__proto__,instance.constructor將再也不爲Object

instance.__proto__ = null
instance.constructor === Object // false
複製代碼

★ 15 - 原型鏈

原型鏈是由相互關聯的原型對象組成的鏈狀結構

每一個對象都有__proto__屬性(構造函數也有),指向建立該對象的構造函數的原型__proto__將對象連接起來組成了原型鏈。是一個能夠用來實現繼承和共享屬性的有限鏈

原型鏈中的機制

  • 屬性查找機制 當查找對象的屬性時,若是實例對象自身不存在該屬性,則沿着原型鏈往上一級查找,找到時則輸出,不存在時,則繼續沿着原型鏈往上一級查找,直至最頂級的原型對象Object.prototypeObject.prototype.__proto__ === null),假如仍是沒有找到,則輸出undefined
  • 屬性修改機制 只會修改實例對象自己的屬性,若是不存在,則進行添加該屬性,若是須要修改原型的屬性,則須要經過prototype屬性(b.prototype.B = 1),但這樣修改會致使全部繼承於這個對象的實例的屬性發生改變


16 - 原型如何實現繼承?Class如何實現繼承?Class本質是什麼?

  • 組合繼承 使用原型鏈實現對原型屬性和方法的繼承,使用借用構造函數來實現對實例屬性的繼承
function Parent(value){
    this.val = value // 實例屬性
}
Parent.prototype.getValue = function(){ // 原型屬性
    console.log(this.val)
}
function Child(value){
    Parent.call(this,value) // 借用構造函數來繼承實例屬性
}
Child.prototype = new Parent() // 原型鏈繼承
const child = new Child(1)
child.getValue() // 1
child instancof Parent // true
複製代碼

這種方式優勢在於構造函數能夠傳參,不會與父類共享引用屬性,能夠複用父類的函數,但缺點就是在繼承父類函數的時候調用父類構造函數,致使子類的原型上會多了不須要的父類屬性,存在內存浪費

  • 寄生組合繼承
function Parent(value){
    this.val = value
}
Parent.prototype.getValue = function(){
    console.log(this.val)
}
function Child(value){
    Parent.call(this,value)
}
Child.prototype = Object.create(Parent.prototype,{
    constructor:{
        value:Child,
        enumerable:false.
        writable:true,
        configurable:true
    }
}) // 將父類的原型賦值給子類,並將原型的constructor修改成子類
const child = new Child(1)
child.getValue()  // 1
child instanceof Parent // true
複製代碼

這種寄生組合繼承是對組合繼承進行優化的,核心就是將父類的原型賦值給子類,而且將構造函數設置爲子類,這樣解決了無用的父類屬性問題,還能正確的找到子類的構造函數

Class本質及繼承實現

其實JavaScript中並不存在類的概念,class只是一種語法糖,本質來講仍是函數

class Person{}
Person instanceof Function // true
複製代碼

Class繼承

在ES6中,咱們能夠經過class實現繼承

class Parent{
    constructor(value){
        this.val = value
    }
    getValue(){
        console.log(this.val)
    }
}
class Child extends Parent{
    constructor(value){
        super(value)
    }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
複製代碼

Class實現繼承,核心在於使用extends關鍵字來代表繼承自哪一個父類,而且在子類構造函數中必須調用super


★ 17 - 什麼是做用域和做用域鏈

說到做用域,咱們要先理解什麼是執行上下文

執行上下文 能夠簡單理解爲一個對象,它具備三個類型:

  • 全局執行上下文(caller)
  • 函數執行上下文(callee)
  • eval()執行上下文

經過代碼執行過程來理解執行上下文

  • 建立 全局上下文(global EC)
  • 全局執行上下文(caller)逐行以自上而下的順序執行,遇到函數時,函數執行上下文(callee)push到執行棧頂層
  • 函數執行上下文被激活,成爲active EC,而後開始執行函數中的代碼,全局執行上下文(caller)被掛起
  • 函數執行完後,函數執行上下文(callee)pop移除出執行棧,控制權交回給全局執行上下文(caller),繼續按照自上而下的順序執行代碼

❗ 小知識:

變量對象,是執行上下文中的一部分,能夠抽象爲一種數據做用域

其實也能夠理解爲一個簡單的對象,存儲着該執行上下文中的全部變量和函數聲明(不包括函數表達式)

活動對象(AO)- 當變量對象所處的上下文被激活時(active EC)時,稱爲活動對象
複製代碼

做用域

做用域能夠理解爲當前上下文中聲明的變量和函數的做用範圍,它規定了如何查找變量,也就是當前執行代碼對變量的訪問權限

做用域能夠分爲 塊級做用域函數做用域

做用域特性:

  • 變量提高:一個聲明在函數體內部都是可見的(僅var),函數聲明因爲變量聲明
  • 非匿名自執行函數 ,函數變量爲 只讀 狀態,不能修改
let a = function(){
    console.log(1)
}
(function a(){
  a = 'a'
  console.log(a)
})() // ƒ a() { a = 'a' ; console.log(a) }
複製代碼

做用域鏈

做用域鏈能夠理解爲一組對象列表,由當前環境與上層環境的一系列變量對象組成,所以咱們能夠經過做用域鏈訪問到父級裏面聲明的變量或者函數

做用域鏈由兩部分組成

  • [[scope]]屬性:指向父級變量對象和做用域鏈,也就是包含了父級的[[scope]]活動變量(AO)
  • 活動變量:自身活動變量

因爲[[scope]]包含父級[[scope]]造成鏈狀關係,便自上而下造成鏈式做用域

做用域鏈做用

保證當前執行環境裏,有權訪問的變量和函數是有序的(做用域鏈的變量只能向上訪問變量,訪問到window對象時被終止)

Ps:做用域鏈不容許向下訪問

做用域鏈和原型繼承查找時的區別 - 若是去查找一個普通對象的屬性,可是在當前對象和其原型中都找不到時,會返回undefined;但查找的屬性在做用域鏈中不存在的話就會拋出ReferenceError

18 - script的引入方式

  • HTML頁面經過<script>標籤引入
  • Js動態插入<script>標籤
  • <script defer>延遲加載,元素解析完畢後執行
  • <script async>異步加載,但執行時會阻塞元素渲染

19 - null 與 undefined 有什麼區別?

  • null 表示一個對象被定義了,但值是空值(定義爲空)
  • undefined 表示不存在這個值
typeof null // 'Object'
null 是一個空對象,沒有任何的屬性和方法

typeof undefined // 'undefined'
undefined 是一個表示'無'的原始值或表示缺乏值,例如變量被聲明瞭,但沒有任何賦值時
複製代碼

從內存來看null和undefined,本質區別是什麼?

  • 給一個全局變量賦值爲null,至關給這個屬性分配了一塊空的內存,而後值爲null,Js會回收全局變量爲null的對象
  • 給一個全局變量賦值爲undefined,至關於將這個對象的值清空,可是這個對象依舊存在,若是是給對象的屬性賦值爲undefined,說明這個值爲空值

20 - 什麼是內存泄漏?如何解決內存泄漏?

內存泄漏 在使用一些內存以後,若是後面再也不須要用到這些內存,但沒有將它們及時釋放掉,就稱爲內存泄漏

若是出現嚴重的內存泄漏,那麼有可能使得內存愈來愈大,最終致使瀏覽器崩潰

四種常見的Js內存泄露

  • 意外的全局變量
未定義的變量會在全局對象建立一個新變量

function foo(arg){
    bar = 'I am belong to global' // 未定義,會建立在全局中
}

解決方案:
- 在JavaScript頭部加上`use strict`,使用嚴格模式避免意外的全局變量
複製代碼
  • 被遺忘的定時器或回調函數
定義定時器(setTimeout / setInterval)後沒有移除(clearTimeout / clearInterval)
複製代碼
  • 脫離DOM的引用
  • 閉包
閉包的關鍵是匿名函數能夠訪問父級做用域的變量,讓變量不被回收

若是不及時清除,就會致使內存泄漏
複製代碼

如何解決內存泄漏?

經過GC垃圾回收機制來解決內存泄漏

所謂垃圾回收機制,是指找到內存空間中的垃圾並回收,能夠再次利用這部份內存空間

垃圾回收機制有兩種方式:

  • 引用計數: 當聲明一個變量並將一個引用類型賦值給該變量時,則這個值引用就加1,相反,若是包含這個值的變量又取得另一個值,那麼這個值的引用就減去1,當引用次數變爲0,則說明沒有辦法訪問這個值,因此就能夠把其所佔有的內存空間回收
  • 標記清除: 當變量進入環境時,就標記這個變量爲進入環境,當變量離開環境時就將其標記爲離開環境

21 - Js中的循環方式有哪些?For in 與 For of 有什麼區別?

JavaScript中,咱們能夠採用多種方式實現循環

  • while
  • do...while
  • for
  • for...in
  • for...of

for in 與 for of 的區別

  • for in
-  遍歷對象及其原型鏈上可枚舉的屬性

-  若是用於遍歷數組,除了遍歷其元素外,還會遍歷數組對象自定義的可枚舉屬性及其原型鏈上的可枚舉屬性

-  遍歷對象返回的屬性名和遍歷數組返回的索引都是字符串索引

- 某些狀況下,可能按隨機順序遍歷數組元素
複製代碼
  • for of
-  es6 中添加的循環遍歷語法

-  支持遍歷數組,類數組對象(DOM NodeList),字符串,Map 對象,Set 對象

-  不支持遍歷普通對象

-  遍歷後輸出的結果爲數組元素的值

-  可搭配實例方法 entries(),同時輸出數組的內容和索引
複製代碼
  • 補充:Object.keys
-  返回對象自身可枚舉屬性組成的數組

-  不會遍歷對象原型鏈上的屬性以及 Symbol 屬性

-  對數組的遍歷順序和 for in 一致
複製代碼

Tips: for in更適合遍歷對象,儘可能不用for in來遍歷數組


22 - 關於數組(Array)API總結

迭代相關

  • every()
對每一項運行給定函數,全true則返回true
複製代碼
  • filter()
對數組中每一項運行函數,返回該函數會返回true複製代碼
  • forEach()
對數組每一項運行函數,沒有返回值 (forEach沒法中途跳出forEach循環,breakcontinuereturn都不奏效。)
複製代碼
  • map()
對每一項運行函數,返回每次函數調用的結果組成的數組
複製代碼
  • some()
對每一項運行函數,若是對任一項返回了true,則返回true
複製代碼

其餘

  • join('鏈接符')
經過指定鏈接符生成字符串
複製代碼
  • push/pop
數組尾部推入和彈出,改變原數組,返回操做項
複製代碼
  • shift/unshift
數組頭部彈出和推入,改變原數組,返回操做項
複製代碼
  • sort(fn)/reverse
數組排序(fn定義排序規則)與反轉,改變原數組
複製代碼
  • concat
鏈接數組,不改變原數組,返回新數組(淺拷貝)
複製代碼
  • slice(start,end)
截斷數組,返回截斷後的新數組,不改變原數組
複製代碼
  • splice(start,number,arg...)
從下標start開始,刪除number個元素,並插入arg,返回所刪除元素組成的數組,改變原數組
複製代碼
  • indexOf / lastIndexOf(value, fromIndex)
查找數組元素,返回下標索引
複製代碼
  • reduce / reduceRight(fn(prev, cur), defaultPrev)
歸併數組,prev爲累計值,cur爲當前值,defaultPrev爲初始值
複製代碼

23 - 經常使用字符串(String)API總結

  • concat
鏈接字符串
複製代碼
  • indexOf / lastIndexOf()
檢索字符串、從後向前檢索字符串
複製代碼
  • match / replace / search
找到一個或多個正則表達式的匹配 

替換與正則表達式匹配的子串

檢索與正則表達式匹配的值
複製代碼
  • slice
截取字符串片斷,並在新的字符串中返回被截取的片斷
複製代碼
  • substr(start,length)
從起始索引號提取字符串中指定數目的字符
複製代碼
  • substring(start,stop)
截取字符串中兩個指定的索引號之間的字符。
複製代碼
  • split
用於把一個字符串經過指定的分隔符進行分隔成數組
複製代碼
  • toString()
返回字符串
複製代碼
  • valueOf()
返回某個字符串對象的原始值
複製代碼

24 - map、filter、reduce各有什麼做用

  • map
做用:生成一個數組,遍歷原數組,將每個元素拿出來作一些變化後存入新數組

[1,2,3].map(item => item + 1) // [2,3,4]

另外map的回調接收三個參數,分別是當前元素、索引,原數組

常見題:['1','2','3'].map(parseInt) 結果是什麼?
['1','2','3'].map(parseInt) → [1,NaN,NaN]
解析:
第一輪遍歷 parseInt('1',0) // 1
第二輪遍歷 parseInt('2',1) // NaN
第三輪遍歷 parseInt('3',2) // NaN
複製代碼
  • filter
做用:生成一個新數組,在遍歷數組的時候將返回值爲true的元素放在新數組

場景:咱們能夠利用這個函數刪除一些不須要的元素(過濾)

let array = [1,2,3,4,5]
let newArray = array.filter(item => item !== 5)
console.log(newArray) // [1,2,3,4]

Tips:與map一致,也接收3個參數
複製代碼
  • reduce
做用:將數組中的元素經過回調函數最後轉換爲一個值(歸併)

場景:實現一個將數組裏的元素所有相加獲得一個值的功能

const arr = [1,2,3]
const sum = arr.reduce((acc,current) => {
    return acc + current
},0)
console.log(sum) // 6

對於reduce來講,它只接受2個參數,分別是回調函數和初始值:
- 首先初始值爲0,該值會在執行第一次回調函數時做爲第一個參數傳入
- 回調函數接收四個參數,分別爲累計值、當前元素、當前索引、原數組
- 在第一次執行回調時,當前值和初始值相加爲1,該結果會做爲第二次回調時的累計值(第一個參數)傳入
- 第二次執行時,相加的值分別爲1和2,以此類推,循環結果後得出最終值

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
- total 必選,初始值或計算結束後的返回值
- currentValue 必選,當前元素
- currentIndex 可選,當前索引
- arr 可選,當前元素所屬的數組對象
- initialValue 可選,傳遞給函數的初始值
複製代碼

25 - 代碼複用

  • 函數封裝
  • 繼承
  • 混入mixin
  • 複製extend
  • 借用call/apply

26 - 併發與並行有什麼區別?

  • 併發 是宏觀概念,指在一段時間內經過任務間的切換完成了這個兩個任務,這種狀況就稱爲併發
  • 並行 是微觀概念,同時完成多個任務的狀況就稱爲並行

27 - require 與 import 有什麼區別?

二者區別在於加載方式不一樣、規範不一樣

  • 加載方式不一樣:require是在運行時加載,而import是在編譯時加載
require('./a')() // 假設a模塊是一個函數,當即執行a模塊函數
var data = reuqire('./a').data // 假設a模塊導出一個對象
Tips:require寫在代碼哪一行均可以

import Jq from 'jquery'
import * as _ from '_'
import {a,b,c} from './a'
import {default as alias, a as a_a, b, c} from './a';
Tips:import應該用在代碼行開頭
複製代碼
  • 規範不一樣:require是CommonJS/AMD規範,而import屬於ES6規範

❗ 小知識:

require特色:
- 提供了服務器/瀏覽器的模塊加載方案。非語言層面的標準。
- 只能在運行時肯定模塊的依賴關係及輸入/輸出的遍歷,沒法進行靜態優化

import特色:
- 語言規格層面支持模塊功能。
- 支持編譯時靜態分析,動態綁定
複製代碼

28 - 如何判斷一個變量是NaN?

NaN與任務值比較都是false,包括他本身,判斷一個變量爲NaN,能夠經過isNaN()

isNaN(NaN) // true
複製代碼

29 - 嚴格模式有什麼做用?表如今哪?

  • 做用
- 消除JavaScript語法的一些不合理、不嚴謹之處,減小一些怪異行爲
- 消除代碼運行的一些不安全行爲,保證代碼運行的安全
- 提升編譯器效率,增長運行速度
- 爲將來新版本的JavaScript作好鋪墊
複製代碼
  • 表現
- 嚴格模式下,delete運算符後跟隨非法標識符,會拋出語法錯誤
- 嚴格模式下,定義同名屬性會拋出語法錯誤
- 嚴格模式下,函數形參存在同名,會拋出語法錯誤
- 嚴格模式下,不容許八進制整數直接量
- 嚴格模式下,arguments對象是傳入函數內實參列表的靜態副本
- 嚴格模式下,eval和arguments當作關鍵字,它們不能被賦值和用做變量聲明
- 嚴格模式下,變量必須先聲明,直接給變量賦值,不會隱式建立全局變量,不能用with
- 嚴格模式下,call/apply第一個參數爲null/undefined,不會被轉換爲window
複製代碼

30 - 說說對鬆散類型的理解

JavaScript中的變量是鬆散類型,所謂鬆散類型,就是指當一個變量被聲明出來就能夠保存任何類型的值,一個變量所保存值的類型也能夠改變


★ 31 - 函數防抖(debounce) 與 函數節流(throttle)

若是事件處理函數(click)調用頻率無限制,會加劇瀏覽器的負擔,致使用戶體驗很是糟糕,那麼咱們能夠採用debounce(防抖)throttle(節流) 的方式來減小調用頻率,同時又不影響實際效果

  • 函數防抖(debounce) 當持續觸發事件時,必定時間段內沒有再次觸發事件,事件處理函數才執行一次,若是設當的事件到來以前,又觸發了事件,就從新開始延時(即設定的時間內觸發事件將無效)
例如:
🔺當持續觸發scroll事件時,事件處理函數handle只在中止滾動1000毫秒以後纔會調用一次,即持續觸發滾動事件的過程當中,handle一直沒有執行
複製代碼
function debounce(handle){
    let timeout = null // 建立一個標記用來存放定時器
    return function(){
        clearTimeout(timeout) // 每當用戶調用的時候把前一個定時器清空
        timeout = setTimeout(() => {
            handle.apply(this,arguments)
        },500) // 500ms後觸發,期間再次調用,則從新計算延時
    }
}
function sayHi(){
    console.log('防抖成功')
}
var btn = document.getElementById('button')
btn.addEventListener('click',debounce(sayHi))
複製代碼
  • 函數節流(throttle) 當持續觸發事件時,保證必定時間段內只調用一次事件處理函數 (經過判斷是否到達必定條件來觸發函數)
    • 第一種方式:經過時間戳來判斷是否已到可執行時間,記錄上一次執行的時間戳,而後每次觸發事件執行回調,回調中判斷當前時間戳距離上次執行時間戳的間隔是否已經到達設置的時間差,若是是則執行,並更新上次執行的時間戳
    • 第二種方式:使用定時器
function throttle(handle){
    let canRun = true // 經過閉包保存一個標記,不被回收
    return function(){
        if(!canRun) return // 在函數頭部判斷標記是否爲true,爲false時不容許調用handle
        canRun = false // 設置標記爲false
        setTimeout(() => { // 將外部傳入的函數的執行放在setTimeout中
          fn.apply(this, arguments)
          // 最後在setTimeout執行完畢後再把標記設置爲true(關鍵)表示能夠執行下一次循環了。當定時器沒有執行的時候標記永遠是false,在開頭被return掉
          canRun = true
        }, 500);
    }
}
function sayHi() {
  console.log('節流成功');
}
var btn = document.getElementById('button');
btn.addEventListener('click', throttle(sayHi)); // 節流
複製代碼

32 - 什麼是事件捕獲?什麼是事件冒泡?

  • 事件捕獲:事件從最不精準的目標(document對象)開始觸發,而後到最精確的目標 (不精確 → 精確)
  • 事件冒泡:事件按照從最特定的事件目標到最不特定的事件目標(document對象)的順序觸發 (特定 → 不特定)
哪些事件不支持冒泡?
- 鼠標事件:mouseleave、mouseenter
- 焦點事件:blur、focus
- UI事件:scroll、resize
- ···
複製代碼

image

事件如何先冒泡後捕獲?

對於同一個事件,監聽捕獲和冒泡,分別對應相應的處理函數,監聽到捕獲事件時,先暫停執行,直到冒泡事件被捕獲後再執行事件


33 - 如何阻止事件冒泡?又如何阻止默認行爲?

  • 阻止事件冒泡
非IE瀏覽器:event.stopPropagation()
IE瀏覽器:window.event.cancelBubble = true

function stopBubble(e){
    // 若是提供了事件對象,則是非IE瀏覽器下
    if(e && e.stopPropagation){
        // 所以它支持W3C的stopPropagation()方法
        e.stopPropagation()
    }else{
        // IE瀏覽器下,取消事件冒泡
        window.event.cancelBubble = true
    }
}
複製代碼
  • 阻止默認行爲
非IE瀏覽器:event.preventDefault()
IE瀏覽器:window.event.returnValue  = false

function stopDefault(e) {
  //阻止默認瀏覽器動做(W3C)
  if (e && e.preventDefault) e.preventDefault();
  //IE中阻止函數器默認動做的方式
  else window.event.returnValue = false;
  return false;
}
複製代碼

34 - 事件委託是什麼?

所謂事件委託,就是利用事件冒泡的原理,讓本身所觸發的事件,讓其父元素代替執行

即:不在事件(直接DOM)上設置監聽函數,而是在其父元素上設置監聽函數,經過事件冒泡,父元素能夠監聽到子元素上事件的觸發,經過判斷事件發生在哪個子元素上來作出不一樣的響應

爲何要用事件委託?好處在哪?

  • 提升性能
<ul>
  <li>蘋果</li>
  <li>香蕉</li>
  <li>鳳梨</li>
</ul>

// 在ul上設置監聽函數(Good)
document.querySelector('ul').onclick = (event) => {
  let target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// 在每個li上監聽函數(Bad)
document.querySelectorAll('li').forEach((e) => {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
})
複製代碼
  • 新添加的元素也能觸發綁定在父元素上的監聽事件

事件委託與事件冒泡的對比

- 事件冒泡:父元素下不管是什麼元素,點擊後都會觸發 box 的點擊事件
- 事件委託:能夠對父元素下的元素進行篩選
複製代碼

35 - Js中高階函數是什麼?

高階函數(Highter-order-function)的定義很簡單,就是至少知足下列一個條件的函數:

  • 接受一個或多個函數做爲輸入
  • 輸出一個函數

也就是說高階函數是對其餘函數進行操做的函數,能夠將它們做爲參數傳遞,或者是返回它們。

  • 函數做爲參數傳遞 Javascript中內置了一些高階函數,好比Array.prototype.mapArray.prototype.filterArray.prototype.reduce···,它們接受一個函數做爲參數,並應用這個函數到列表的每個元素
對比使用高階函數和不使用高階函數

例子:有一個數組[1,2,3,4],咱們想要生成一個新數組,其元素是以前數組的兩倍

- 不使用高階函數
const arr1 = [1,2,3,4]
const arr2 = []
for(let i = 0; i < arr1.length; i++){
    arr2.push(arr1[i] * 2)
}

- 使用高階函數
const arr1 = [1,2,3,4]
const arr2 = []
arr2 = arr1.map( item => item * 2)
複製代碼
  • 函數做爲返回值輸出
在判斷類型的時候能夠同個Object.prototype.toString.call來獲取對應對象返回的字符串,如:

let isString = obj => Object.prototype.toString.call( obj ) === '[object String]'
let isArray = obj => Object.prototype.toString.call( obj ) === '[object Array]'
let isNumber = obj => Object.prototype.toString.call( obj ) === '[object Number]'

能夠發現這三行有許多重複代碼,只須要把具體的類型抽離出來就能夠封裝成一個判斷類型的方法,如:

let isType = (type, obj) => {
    return Object.prototype.toString.call(obj) === '[Object ' + type + ']'
}

isType('String')('123');		// true
isType('Array')([1, 2, 3]);	// true
isType('Number')(123);			// true
複製代碼

36 - 什麼是柯里化函數?

柯里化 - 簡單來講就是隻傳遞函數一部分參數來調用它,讓它返回一個新函數去處理剩下的參數。

經過add()函數來了解柯里化

const add = (...args) => args.reduce( (a, b) => a + b )  // a爲初始值或計算結束的返回值,b爲當前元素

// 傳入多個參數,執行add函數
add(1,2) // 3

// 假設咱們實現了一個柯里化函數,支持一次傳入一個參數
let sum = currying(add)
// 封裝第一個參數,方便重用
let addCurryOne = sum(1)
addCurryOne(2) // 3
addCurryOne(3) // 4
複製代碼

實現currying函數

咱們能夠理解所謂的柯里化函數,就是封裝一系列的處理步驟,經過閉包將參數集中起來計算,最後再把須要處理的參數傳進去,那麼如何實現currying函數呢?

實現原理就是用閉包把傳入的參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數

實現一個健壯的currying函數:

function currying(fn, length) {
    length = length || fn.length // 第一次調用獲取函數 fn 參數的長度,後續調用獲取 fn 剩餘參數的長度
    return function( ...args ) { // currying 包裹以後返回一個新函數,接收參數爲 ...args
        return args.length >= length // 新函數接收的參數長度是否大於等於 fn 剩餘參數須要接收的長度
            ? fn.apply(this, args) // 知足要求,執行 fn 函數,傳入新函數的參數
            : currying(fn.bind(this, ...agrs), length - args.length) // 不知足要求,遞歸 currying 函數,新的 fn 爲 bind 返回的新函數(bind 綁定了 ...args 參數,未執行),新的 length 爲 fn 剩餘參數的長度
    }
}

// Test
const fn = currying(function(a,b,c) {
  console.log([a, b, c])  
})

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
複製代碼

實際應用

  • 延遲計算:部分求和、bind函數
延遲計算:

const add = (...args) => args.reduce((a, b) => a + b);

// 簡化寫法
function currying(func) {
    const args = [];
    return function result(...rest) {
        if (rest.length === 0) {
          return func(...args);
        } else {
          args.push(...rest);
        	return result;
        }
    }
}

const sum = currying(add);

sum(1,2)(3); // 未真正求值
sum(4); 		 // 未真正求值
sum(); 			 // 輸出 10
複製代碼
  • 動態建立函數:添加監聽addEvent、惰性函數
  • 參數複用

37 - 一行代碼將數組扁平化並去重,最終獲得升序不重複的數組

Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b}
複製代碼
解析:
array.from - 從一個類數組或可迭代對象建立一個新的,淺拷貝的數組實例
array.flat - 用於將嵌套的數組'拉平'(扁平化)
    [1,2,[3,4]].flat() // [1,2,3,4]
array.sort - 用於對數組的元素進行排序
    - 該函數要比較兩個值,而後返回一個用於說明這兩個值的相對順序的數字。
      比較函數應該具備兩個參數 a 和 b,其返回值以下:
        - 若 a 小於 b,在排序後的數組中 a 應該出如今 b 以前,則返回一個小於 0 的值。
        - 若 a 等於 b,則返回 0
        - 若 a 大於 b,則返回一個大於 0 的值。
        
1 - arr.flat(Infinity) 先將數組扁平化
2 - new Set(arr.flat(Infinity)) 去重扁平化後的數組
3 - Array.from(new Set(arr.flat(Infinity))) 建立一個新的,淺拷貝的數組實例
4 - Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b } 將該新數組進行升序排序
複製代碼

38 - JS如何動態添加、移除、移動、複製、建立和查找節點?

  • 建立新節點
createDocumentFragment() - 建立一個DOM片斷
createElement() - 建立一個具體的元素
createTextNode() - 建立一個文本節點
複製代碼
  • 添加、移除、替換、插入
appendChild() - 添加子節點
removeChild() - 移除子節點
replaceChild() - 替換子節點
insertBefore() - 插入
複製代碼
  • 查找
getElementsByTagName() - 經過標籤名稱
getElementsByName() - 經過元素的Name屬性
getElementById() - 經過元素id,具備惟一性
複製代碼

39 - Javascript是一門怎樣的語言?有什麼特色?

  • 腳本語言Javascript是一種解釋型語言,C、C++等語言先編譯後執行,而Javascript是在程序的運行過程當中逐行進行解釋
  • 基於對象Javascript是一種基於對象的腳本語言,它不只能夠建立對象,也能使用現有的對象
  • 簡單Javascript語言中採用的是弱類型的變量類型,對使用的數據類型未作出嚴格的要求,是基於Java基本語句和控制的腳本語言
  • 動態性Javascript是一種採用事件驅動的腳本語言,它不須要通過Web服務器就能夠對用戶的輸入作出響應
  • 跨平臺性Javascript不依賴於操做系統,僅須要瀏覽器的支持

40 - 兼容各類瀏覽器版本的事件綁定

/**
 * 兼容低版本IE,element爲須要綁定事件的元素,
 * eventName爲事件名(保持addEventListener語法,去掉on),fun爲事件響應函數
 */

function addEvent(element, eventName, fun){
    if(element.addEventListener){
        element.addEventListener(eventName, fun, false)
    }else{
        ele.attachEvent('on' + eventName, fun)
    }
}
複製代碼

41 - sort()排序原理是什麼?

sort()內部是利用遞歸進行冒泡排序的

  • 解析冒泡排序的原理
- 比較相鄰的元素,若是第一個比第二個大,就交換它們兩個

- 對每一對相鄰元素作一樣的工做,從開始第一對到結尾的最後一對,在這一點,最後的元素應該會是最大的數

- 針對全部的元素重複以上的步驟,除了最後一個

- 持續每次對愈來愈少的元素重複上面的步驟,知道沒有任何一對數字須要比較
複製代碼
  • 示例
var arr = [1,5,4,2]

sort()的比較邏輯爲:
- 1和5比,1和4比,1和2比
- 5和4比,5和2比
- 4和2比
複製代碼
  • sort()排序規則
- return > 0 則交換數組相鄰2個元素的位置
- arr.sort(function(a,b){ ... })
    - a → 表明每一次執行匿名函數時,找到數組中的當前項
    - b → 表明當前項的後一項
複製代碼
1 - 升序
var arr = [45, 42, 10, 147, 7, 65, -74]
console.log(arr.sort()) → 默認法 缺點:默認排序順序是根據字符串UniCode碼。由於排序是按照字符串UniCode碼的順序進行排序的(按首位排序)
// [-74, 10, 147, 42, 45, 65, 7]

console.log(
  arr.sort(function(a, b) {
    return a - b; // 若return返回值大於0(即a>b),則a,b交換位置
  }) → 指定排序規則,return可返回任何值
)
// [-74, 7, 10, 42, 45, 65, 147]


2 - 降序
var arr = [45, 42, 10, 111, 7, 65, -74];
console.log(
  arr.sort(function(a, b) {
    return b - a; // 若return返回值大於零(即b>a),則a,b交換位置
  }) → 指定排序規則,return可返回任何值
);
複製代碼

42 - 如何判斷當前腳本運行在瀏覽器仍是node環境中?

經過判斷Global對象是否爲window,若是不爲window,則當前腳本沒有運行在瀏覽器中
複製代碼

43 - 一行代碼求數組最大值與最小值

var a = [1, 2, 3, 5];
alert(Math.max.apply(null, a)); //最大值
alert(Math.min.apply(null, a)); //最小值

之因此須要用到apply,是由於 Math.max / Math.min 不支持傳遞數組過去
複製代碼

44 - offsetWidth/offsetHeight,clientWidth/clientHeight 與 scrollWidth/scrollHeight 的區別

  • offsetWidth → 返回元素的寬度(包括元素寬度、內邊距和邊框,不包括外邊距)

  • offsetHeight → 返回元素的高度(包括元素高度、內邊距和邊框,不包括外邊距)

  • clientWidth → 返回元素的寬度(包括元素寬度、內邊距,不包括邊框和外邊距)

  • clientHeight → 返回元素的高度(包括元素高度、內邊距,不包括邊框和外邊距)

  • style.width → 返回元素的寬度(包括元素寬度,不包括內邊距、邊框和外邊距)

  • style.height → 返回元素的高度(包括元素高度,不包括內邊距、邊框和外邊距)

  • scrollWidth → 返回元素的寬度(包括元素寬度、內邊距和溢出尺寸,不包括邊框和外邊距),無溢出的狀況,與clientWidth相同

  • scrollHeigh → 返回元素的高度(包括元素高度、內邊距和溢出尺寸,不包括邊框和外邊距),無溢出的狀況,與clientHeight相同


45 - offsetTop / offsetLeft / scrollTop / scrollLeft 的區別

  • offsetTop → 返回元素的上外緣距離最近採用定位父元素內壁的距離,若是父元素中沒有采用定位的,則是獲取上外邊緣距離文檔內壁的距離。

  • offsetLeft → 此屬性和offsetTop的原理是同樣的,只不過方位不一樣

  • scrollLeft → 此屬性能夠獲取或者設置對象的最左邊到對象在當前窗口顯示的範圍內的左邊的距離,也就是元素被滾動條向左拉動的距離。

  • scrollTop → 此屬性能夠獲取或者設置對象的最頂部到對象在當前窗口顯示的範圍內的頂邊的距離,也就是元素滾動條被向下拉動的距離。


46 - Javascript中的arguments對象是什麼?

在函數調用的時候,瀏覽器每次都會傳遞進兩個隱式參數,一個是函數的上下文對象this,另一個則是封裝實參的僞數組對象arguments

關於arguments

  • arguments定義是對象,可是由於對象的屬性是無序的,而arguments是用來存儲實參的,是有順序的,它具備和數組相同的訪問性質及方式,並擁有數組長度屬性length(類數組對象、用來存儲實際傳遞給函數的參數)
  • arguments訪問單個參數的方式與訪問數組元素的方式相同,例如arguments[0]arguments[1]arguments[n],在函數中不須要明確指出參數名,就能訪問它們。經過length屬性能夠知道實參的個數。
  • arguments有一個callee屬性,返回正被執行的Function對象
function fun() {
    console.log(arguments.callee === fun); // true
}
fun();
複製代碼
  • 在正常模式下,arguments對象是容許在運行時進行修改
function fun() {
    arguments[0] = 'sex';
    console.log(arguments[0]); // sex
}
fun('name', 'age');
複製代碼

一行代碼實現僞數組arguments轉換爲數組

var args = [].slice.call(arguments)
複製代碼

★ 47 - Js的事件循環(Event Loop)機制

爲何Js是單線程?

Javascript做爲主要運行在瀏覽器的腳本語言,主要用途之一就是操做Dom

若是Javascript同時有兩個線程,同時對同一個Dom進行操做,這時瀏覽器應該聽哪一個線程的,又如何判斷優先級?

爲了不這種問題,Javascript必須是一門單線程語言

執行棧與任務隊列

因爲Javascript是單線程語言,當遇到異步任務(如Ajax)時,不可能一直等到異步執行完成後,再繼續往下執行,由於這段時間瀏覽器處於空閒狀態,會致使巨大的資源浪費

執行棧

當執行某個函數、事件(指定過回調函數)時,就會進入執行棧中,等待主線程讀取

執行棧可視化:

主線程

主線程與執行棧不一樣,主線程規定了如今執行執行棧中的哪一個事件

主線程循環: 即主線程會不停的從執行棧中獲取事件,而後執行完全部棧中的同步代碼

當遇到一個異步事件後,並不會一直等待異步事件返回結果,而是會將這個事件掛在與執行棧不一樣的隊列中,這個隊列稱爲任務隊列TaskQueue

當主線程將執行棧中的全部代碼都執行完後,主線程將會去查看任務隊列中是否存在任務。 若是存在,那麼主線程會依次執行那些任務隊列中的回調函數

Javascript異步執行的運行機制
  • 全部任務都在主線程上執行,造成一個執行棧
  • 主線程以外,還存在一個任務隊列(TaskQueue。只要異步任務有了返回結果,就在任務隊列之中放置一個事件
  • 當執行棧中的全部同步任務執行完畢,就會去查看任務隊列,那些對應的異步任務,結束等待狀態,進入執行棧並開始執行
  • 主線程會不斷的重複第三點

宏任務與微任務

異步任務能夠分爲兩類,不一樣類型的API註冊的任務會依次進入到各自對應的隊列中,而後等待事件循環(EventLoop)將它們依次壓入執行棧中執行

  • 宏任務MacroTask
script(總體代碼),setTimeout,setInterval,setImmediate,UI渲染,I/O流操做,postMessage,MessageChannel
複製代碼
  • 微任務MicroTask
Promise,MutaionObserver,process.nextTick
複製代碼

事件循環(EventLoop)

Event Loop(事件循環)中,每一次循環稱爲 tick, 每一次tick的任務以下:

  • 執行棧選擇最早進入隊列的宏任務(一般是script總體代碼),若是有則執行
  • 檢查是否存在 Microtask,若是存在則不停的執行,直至清空 microtask 隊列
  • 更新render(每一次事件循環,瀏覽器均可能會去更新渲染)
  • 重複以上步驟

綜上所述

宏任務 → 全部微任務 → 下一個宏任務
複製代碼

兩道題檢驗是否已經 get√

題 1:

setTimeout(function () {
    console.log(1)
});
new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val)
})
console.log(4)
複製代碼
Result:
2 → 4 → 3 → 1
複製代碼
題 2:

new Promise(resolve => {
    resolve(1);
    
    Promise.resolve().then(() => {
    	// t2
    	console.log(2)
    });
    console.log(4)
}).then(t => {
	// t1
	console.log(t)
});
console.log(3);
複製代碼
Result:
4 → 3 → 2 → 1
複製代碼
解析:
- script任務運行,首先遇到Promise實例,執行構造函數,輸出4,此時微任務隊列中有t2和t1
- script任務繼續執行同步代碼,輸出3後第一個宏任務執行完成
- 執行全部的微任務,即輸出2和1

??? 爲何t2會比t1先執行 ???
- 根據 Promises/A+ 規範
- Promise.resolve 方法容許調用時不帶參數,直接返回一個resolved 狀態的 Promise 對象
- 當即 resolved 的 Promise 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時
複製代碼

48 - 異步編程的六種方式

Javascript是單線程工做,也就是隻有一個腳本執行完以後才能夠執行下一個腳本,兩個腳本不能同時執行,那麼若是腳本耗時很長,後面的腳本都必須排隊等待,會拖延整個程序的執行

異步編程的六種方式

  • 回調函數 - 假如f1是一個須要必定時間的函數,因此能夠將f2寫成f1的回調函數,將同步操做變成操做,f1不會阻塞程序的運行,f2也不需等待
function f1(cb){
    setTimeout(() => {
        console.log('f1')
    })
    cb()
}
function f2(){
    console.log('f2')
}
f1(f2) // f2 → f1
複製代碼
function fn(a,b,cb){
    var num = Math.ceil(Math.random() * (a - b) + b)
    cb(num)
}
fn(10,20,function(num){
    console.log("隨機數" + num);
})  // 10 ~ 20 的隨機數
複製代碼
總結:
- 回調函數易於實現,便於理解,可是屢次回調會致使代碼高度耦合
- 回調函數定義:函數A做爲參數(函數引用)傳遞到另一個函數B,而且這個函數B執行函數A,咱們就叫函數A叫作回調函數,若是沒有名稱(函數表達式),咱們就叫它匿名回調函數
- 回調函數優勢:簡單,容易理解
- 回調函數缺點:不利於代碼的閱讀和維護,各部分之間高度耦合,並且每個任務只能指定一個回調函數
複製代碼
  • 事件監聽 - 採用事件驅動模式,腳本的執行不取代代碼的順序,而取決於某一個事件是否發生
監聽函數有:on、bind、listen、addEventListener、observe
複製代碼
優勢:容易理解,能夠綁定多個事件,每個事件能夠接收多個回調函數,並且能夠減小耦合,利於模塊化

缺點:整個程序都要變成事件驅動型,運行流程會變得不清晰
複製代碼
element.onclick = function(){
    // toDo
}

Or:

element.onclick = handler1
element.onclick = handler2
element.onclick = handler3

缺點:
當同一個element元素綁定多個事件時,只有最後一個事件會被添加,上述只有handler3會被添加執行
複製代碼
elment.attachEvent("onclick", handler1)
elment.attachEvent("onclick", handler2)
elment.attachEvent("onclick", handler3)

Result: 3 → 2 → 1

elment.addEventListener("click", handler1, false)
elment.addEventListener("click", handler2, false)
elment.addEventListener("click", handler3, false)

Result:1 → 2 → 3
(PS:該方法的第三個參數是泡沫獲取,是一個布爾值:當爲false時表示由裏向外,true表示由外向裏。)
複製代碼
DOM - addEventListener()和removeListener()

addEventListenner()和removeListenner()表示用來分配和刪除事件的函數。這兩種方法都須要三種參數,分別爲:
- 事件名稱(String)
- 觸發事件的回調函數(function)
- 指定事件的處理函數的時期或階段(boolean)
複製代碼
  • 觀察者模式(Observe) - 也稱爲發佈訂閱模式

定義了一種一對多的關係,讓多個觀察者同時監聽某一個主題對象,這一個主題對象一旦發生狀態變化,就會通知全部觀察者對象,使得它們可以自動更新本身

優勢:
- 支持簡單的廣播通訊,自動通知全部已經訂閱過的對象
- 頁面載入後,目標對象很容易與觀察者存在一種動態關聯,增長靈活性
- 目標對象與觀察者之間的抽象耦合關係可以單獨擴展以及重用
複製代碼
  • Promise
- promise對象是commonJS工做組提出的一種規範,一種模式,目的是爲了異步編程提供統一接口
- promise是一種模式,promise能夠幫忙管理異步方式返回的代碼。他將代碼進行封裝並添加一個相似於事件處理的管理層。咱們可使用promise來註冊代碼,這些代碼會在在promise成功或者失敗後運行
- promise完成以後,對應的代碼也會執行。咱們能夠註冊任意數量的函數再成功或者失敗後運行,也能夠在任什麼時候候註冊事件處理程序
- promise有兩種狀態:一、等待(pending);二、完成(settled)
- promise會一直處於等待狀態,直到它所包裝的異步調用返回/超時/結束
- 這時候promise狀態變成完成。完成狀態分紅兩類:一、解決(resolved);二、拒絕(rejected)
- promise解決(resolved):意味着順利結束。promise拒絕(rejected)意味着沒有順利結束
複製代碼
  • Generator - Generator函數是一個狀態機,封裝了多個內部狀態
  • async

49 - 同源策略

Javascript只能與同一個域中的頁面進行通信

兩個腳本被認爲是同源的條件:

  • 協議相同
  • 端口相同
  • 域名相同

50 - jsonP的優缺點

  • 優勢
    • 它不像XMLHttpRequest對象實現的AJAX請求那樣受到同源策略的限制,jsonP能夠實現跨越同源策略
    • 它的兼容性更好,在更加古老的瀏覽器中均可以運行,不須要XMLHttpRequestActiveX的支持
    • 在請求完畢後能夠經過調用callback的方式回傳結果。將回調方法的權限給了調用方。
  • 缺點
    • 它只支持GET請求而不支持POST等其餘類型的HTTP請求
    • 它只支持跨域HTTP請求這種狀況,不能解決不一樣域的兩個頁面之間如何進行JavaScript調用問題
    • JsonP在調用失敗的時候不會返回各類http狀態碼
    • 缺點是安全性。萬一假如提供JsonP的服務存在頁面注入漏洞(即它所返回的javascript的內容被人控制),那麼結果是什麼?全部調用這個JsonP的網站都會存在漏洞,缺少安全。

★ 51 - AJAX

什麼是AJAX,爲何使用AJAX?

  • AJAX是一種建立交互式網頁應用的網頁開發技術
  • AJAX能夠實如今沒必要刷新整個頁面的狀況下實現局部更新,與服務器進行異步通信的技術

XMLHttpRequest對象

XMLHttpRequest對象能夠說是AJAX的核心對象,是一種支持異步請求的技術。即XMLHttpRequest使你可使用javascript向服務器提出請求並作出響應,又不會致使阻塞用戶。經過XMLHttpRequest對象,能夠實如今不刷新整個頁面的狀況下實現局部更新

XMLHttpRequest對象的常見屬性
  • onreadystatechange - 一個Js函數對象,當readyState屬性改變時會調用它(請求狀態改變的事件觸發器)
  • readyState - Http請求的狀態,當一個XMLHttpRequest初次建立時,這個屬性的值從0開始,直到接收到完整的Http響應,這個值增長到4
    • 0 - 初始化狀態。XMLHttpRequest 對象已建立或已被abort()方法重置。
    • 1 - open()方法已調用,可是send() 方法未調用。請求尚未被髮送
    • 2 - send()方法已調用,HTTP 請求已發送到 Web 服務器,但未接收到響應
    • 3 - 全部響應頭部都已經接收到,響應體開始接收但未完成
    • 4 - Http響應已經徹底接收
readyState 的值不會遞減,除非當一個請求在處理過程當中的時候調用了 abort() 或 open() 方法

每次這個屬性的值增長的時候,都會觸發 onreadystatechange 事件句柄。
複製代碼
  • status - 由服務器返回的 HTTP 狀態代碼,如 200 表示成功,而 404 表示 "Not Found" 錯誤。當 readyState 小於 3 的時候讀取這一屬性會致使一個異常。
關於Http狀態碼,常見以下:

1) 1XX 通知
2) 2XX 成功
3) 3XX 重定向
4) 4XX 客戶端錯誤
5) 5XX 服務端錯誤

最基本的響應狀態:

- 200('ok') : 服務器已成功處理了請求
- 400('bad request'):服務器沒法解析該請求
- 500('Internal Server Error'):服務器內部錯誤服務器遇到錯誤,沒法完成請求
- 301('Moved Permanently'):永久移動請求的網頁已永久移動到新位置,即永久重定向
- 404('Not Found'):未找到服務器找不到請求的網頁
- 409('Conflict'):服務器在完成請求時發生衝突
複製代碼
XMLHttpRequest對象的常見API
  • Open() - 建立http請求
    • 第一個參數:定義請求的方式(get/post)
    • 第二個參數:提交的地址url
    • 第三個參數:指定異步/同步(true → 異步,false → 同步)
    • 第四第五個參數:http認證
在一個已經激活的request下(已經調用open()或者openRequest()方法的請求)再次調用這個方法至關於調用了abort()方法。
複製代碼
  • setRequestHeader() - 向一個打開但未發送的請求設置或添加一個Http請求(設置請求頭)
    • 第一個參數:將要被賦值的請求頭名稱(header)
    • 第二個參數:給指定的請求頭賦值(value)
  • send() - 發送http請求,使用傳遞給open()方法的參數,以及傳遞給該方法的可選請求體
    • 若是爲get,參數爲null / 若是爲post,參數爲提交的參數
  • abort() - 取消當前響應
  • getAllResponseHeaders() - 把Http響應頭部做爲未解析的字符串返回
  • getResponseHeader() - 返回指定的 HTTP 響應頭部的值
    • 其參數是要返回的 HTTP 響應頭部的名稱。可使用任何大小寫來制定這個頭部名字,和響應頭部的比較是不區分大小寫的
AJAX的流程是怎麼樣的?
  • 建立XMLHttpRequest對象
  • 定義Http對象
  • 能夠設置Http請求的請求頭
  • 設置響應狀態改變的事件回調函數
  • 發送請求
  • 獲取異步調用返回的數據
  • 使用Js和DOM進行局部解析
原生實現一個Ajax
var ajax = {}

// 兼容性建立httpRequest
ajax.httpRequest = function(){

    // 判斷是否支持XMLHttpRequest
    if(window.XMLHttpRequest){
        return new XMLHttpRequest()
    }
    
    // 兼容 Ie
    var versions = [
        "MSXML2.XmlHttp.6.0",
        "MSXML2.XmlHttp.5.0",
        "MSXML2.XmlHttp.4.0",
        "MSXML2.XmlHttp.3.0",
        "MSXML2.XmlHttp.2.0",
        "Microsoft.XmlHttp"
    ]
    
    // 定義局部xhr,存儲Ie瀏覽器的ActiveXObject對象
    var xhr
    for (var i = 0; i < versions.length; i++) {
        try {
            xhr = new ActiveXObject(versions[i]);
            break;
        } catch (e) {
        }
    }
    return xhr
}

ajax.send = function(url, callback, method, data, async){
    // 默認異步
    if(async === undefined){
        async = true
    }
    
    var httpRequest = ajax.httpRequest()
    
    // 建立Http請求(open)
    httpRequest.open(method, url, async)
    
    // 請求狀態改變的事件觸發器
    httpRequest.onreadystatechange = function(){
        // readyState變爲4時,從服務器拿到數據
        if(httpRequest.readyState === 4){
            callback(httpRequest.responseText)
        }
    }
    
    // 設置http請求的請求頭(setRequestHeader)
    if (method == 'POST') {
          //給指定的HTTP請求頭賦值
        httpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
    }
    
    // 發送Http請求
    httpRequest.send(data)
}

// 封裝GET/POST請求
ajax.get = function (url, data, callback, async) {
    var query = [];
    for (var key in data) {
        query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
    }
    ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
}
ajax.post = function (url, data, callback, async) {
    var query = [];
    for (var key in data) {
        query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
    }
    ajax.send(url, callback, 'POST', query.join('&'), async)
}
複製代碼

52 - 如何解決跨域問題?

  • CORS → 服務端設置請求頭(Access-Control-Allow-Origin
  • jsonP → 動態加載<script>標籤(只能解決GET請求)
  • window.name → 利用瀏覽器窗口內,載入全部的域名都是共享一個window.name
  • ducment.domain / window.postMessage()

53 - document.Ready()、onload、前寫JS有什麼區別?

  • document.Ready()能夠定義多個函數,按照綁定的順序進行執行,而onload只能執行一次,定義多個onload,後面的會覆蓋前面的
  • onload()是在頁面全部元素都加載完後才執行
  • document.Ready()是在DOM繪製完就執行,沒必要等到徹底加載完再執行
  • </body>前寫Js則是運行到就開始執行,無論有沒有加載完成,因此有可能出現Js操做節點時獲取不到該節點

54 - Object.freeze()有什麼用?與const有什麼區別?

  • Object.freeze適用於對象值,它可以讓對象不可變(該對象屬性不能修改)
let foo = {
    a:'A'
}
let bar = {
    b:'B'
}
Object.freeze(foo)
foo.a = 'a'
console.log(foo)  // {a: "A"}
複製代碼

相比const,二者是不一樣的概念,const做用是聲明變量(一個只讀變量),一旦聲明,這個變量就不能修改,而Object.freeze()做用是讓一個對象不可變


55 - 細品new

  • new - 配合構造函數建立對象
function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
}

var person = new Person('CHICAGO', 21, 'itMan')
複製代碼
  • 經過例子細品new在建立對象的過程當中作了哪4件事
function Person(){
    this.name = 'CHICAGO'
}
new Person()

- 建立一個空對象 → var obj = {}
- 將空對象賦給this → this = obj
- 將空對象的 __proto__ 屬性指向構造函數的 prototype → this.__proto__ = Person().prototype
- 返回這個對象(this)→ return this
複製代碼
  • 總結
    • 建立空對象{}
    • 將空對象分配給this
    • 將空對象的__proto__指向構造函數的prototype
    • 若是沒有使用顯式return語句,則返回this

56 - in 運算符和 Object.hasOwnProperty 方法有什麼區別?

  • hasOwnProperty() - 返回一個布爾值,判斷對象是否包含特定的自身(非繼承)屬性
判斷自身屬性是否存在

Object.prototype.c= 'C';

var obj = new Object()
obj.a = 'A'

function changeObj(){
    obj.b = 'B'
    delete obj.a
}


obj.hasOwnProperty('a')  // true
obj.hasOwnProperty('c')  // false
changeObj()
obj.hasOwnProperty('a')  // false
obj.hasOwnProperty('b')  // false
複製代碼

若是在函數原型上定義一個變量,hasOwnProperty()方法會直接忽略掉

  • in 運算符

若是指定的屬性在指定的對象或其原型鏈中,則in運算符返回true

Object.prototype.c= 'C';

var obj = new Object()
obj.a = 'A'

console.log('a' in obj)  // true
console.log('c' in obj)  // true
複製代碼

in運算符會檢查它或者其原型鏈是否包含具備指定名稱的屬性


57 - 如何建立一個沒有原型(prototype)的對象?

  • 經過Object.create()能夠實現建立沒有原型的對象
const objHavePrototype = {}
console.log(objHavePrototype.toString())  // [Object object]

const objHaveNoPrototype = Object.create(null)
console.log(objHaveNoPrototype.toString) // TypeError: objHaveNoPrototype.toString is not a function
typeof objHaveNoPrototype // object
複製代碼

咱們知道 typeof null === 'object',但 null 並無 prototype 屬性


58 - 如何判斷一個元素是否使用了event.preventDefault()

  • 經過在事件對象中使用event.defaultPrevented屬性,該屬性返回一個布爾值用於區分是否在特定元素中使用了event.preventDefault()

59 - 訪問不存在的屬性,爲何有時返回undefined,有時倒是報錯

var foo = {}

console.log(foo.a)  // undefined
console.log(foo.a.A)  // TypeError: Cannot read property 'A' of undefined
複製代碼

觀察上面這個例子,有人會認爲都是返回undefined或者都是報錯,當咱們訪問foo.a的時候,因爲foo對象並不存在a屬性,因此返回的是undefined,而當咱們去訪問一個undefined的屬性時,就會報出TypeError: Cannot read property 'XXX' of undefined的錯誤


60 - 爲何 0.1 + 0.2 != 0.3 ? 如何解決這個問題 ?

因爲計算機是經過二進制來存儲東西,那麼0.1在二進制中會表示爲

// (0011) 表示循環
0.1 = 2^-4 * 1.10011(0011)
複製代碼

能夠發現,0.1在二進制中是一個無限循環的數字,並非精確的0.1,其實不少十進制小數用二進制表示都會是無限循環的,由於Javascript採用浮點數標準,致使會裁剪掉咱們的數字,那麼這些循環的數字被裁剪以後,就會出現精度丟失的問題,也就形成0.1再也不是0.1,而是變成0.100000000000000002

0.100000000000000002 === 0.1 // true
複製代碼

天然,0.2在二進制中也是無限循環,因此

0.1 + 0.2 === 0.30000000000000004 // true
複製代碼

解決 0.1 + 0.2 != 0.3

  • parseFloat(str) - 解析一個字符串,並返回一個浮點數
    • 該函數指定字符串中的首個字符是不是數字。若是是,則對字符串進行解析,直到到達數字的末端爲止,而後以數字返回該數字,而不是做爲字符串
    • str - 必需,要被解析的字符串
  • toFixed(num) - 把Number四捨五入爲指定小數位數的數字
    • num - 必需,規定小數的位數(0 ~ 20)
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
複製代碼

舒適提示😀

  • 因爲Javascript內容較多,本章列舉了較爲重要的部分,我的會繼續總結知識,對該章持續更新,後續會總結Js重點手寫題,建議對本文進行收藏
  • 下一期 - 總結ES6核心知識點
相關文章
相關標籤/搜索