首頁  >  文章  >  web前端  >  JavaScript萬字面試總結

JavaScript萬字面試總結

WBOY
WBOY轉載
2022-10-10 14:43:001277瀏覽

這篇文章為大家帶來了關於JavaScript的相關知識,其中主要介紹了一些面試常見的問題總結,下面一起來看一下,希望對大家有幫助。

JavaScript萬字面試總結

【相關推薦:JavaScript影片教學web前端

一、JS基礎

1.1 Js有哪些資料型別

JavaScript共有八種資料型別

基本資料型別: Undefined、Null、Boolean、 Number、String、Symbol、BigInt。

複雜資料類型:Object

其中Symbol 和BigInt 是ES6 中新增的資料類型:

  • Symbol 代表創建後獨一無二且不可變的資料類型,它主要是為了解決可能出現的全域變數衝突的問題。

  • BigInt 是一種數字類型的數據,它可以表示任意精確度格式的整數,使用BigInt 可以安全地儲存和操作大整數,即使這個數已經超出了Number 能夠表示的安全整數範圍。

1.2 說說你對堆區和堆疊區的理解

在作業系統中,記憶體被分為堆疊區和堆疊區

  • 堆疊區記憶體由編譯器自動分配釋放,存放函數的參數值,局部變數的值等。其操作方式類似資料結構中的堆疊。

  • 堆區記憶體一般由開發分配釋放,若開發者不釋放,程式結束時可能由垃圾回收機制回收。

在資料結構中:

  • 在資料結構中,堆疊中資料的存取方式為進階後出。

  • 堆是一個優先權佇列,是依優先權來進行排序的,優先權可以依照大小來規定。

資料的儲存方式

  • 原始資料型別直接儲存在堆疊(stack)中的簡單資料段,佔據空間小、大小固定,屬於被頻繁使用數據,所以放入堆疊中儲存;

  • 引用資料類型儲存在堆(heap)中的對象,佔據空間大、大小不固定。如果儲存在堆疊中,將會影響程式運行的效能;引用資料類型在堆疊中儲存了指針,該指針指向堆中該實體的起始位址。當解釋器尋找引用值時,會先檢索其在堆疊中的位址,取得位址後會從堆疊中取得實體。

1.3 資料型別偵測的方式有哪些

然後判斷資料型別的方法一般可以透過:typeof、 instanceof、constructor、toString四種常用方法

JavaScript萬字面試總結

#1.4 判斷陣列的方式有哪些

  • 透過Object.prototype.toString.call()做判斷

  • #透過原型鏈做判斷

  • 透過ES6的Array .isArray()做判斷

  • 透過instanceof做判斷

  • 透過Array.prototype.isPrototypeOf

################################ ########1.5 null和undefined區別#############首先Undefined 和Null 都是基本資料類型,這兩個基本資料類型分別都只有一個值,就是undefined 和null。 ######undefined 代表的意思是未定義,null 代表的意思是空物件。一般變數宣告了但還沒定義的時候會回傳 undefined,null主要用來賦值給一些可能會傳回物件的變量,作為初始化。 ######undefined 在 JavaScript 中不是保留字,這表示可以使用 undefined 來作為變數名,但是這樣的做法是非常危險的,它會影響對 undefined 值的判斷。我們可以透過一些方法來獲得安全的 undefined 值,比如說 void 0。 ######當對這兩種類型使用 typeof 進行判斷時,Null 類型化會傳回 “object”,這是一個歷史遺留的問題。當使用雙等號對兩種類型的值進行比較時會傳回 true,使用三個等號時會傳回 false。 ############1.6 typeof null 的結果是什麼,為什麼? ############typeof null 的結果是Object。 ######在 JavaScript 第一個版本中,所有值都儲存在 32 位元的單元中,每個單元包含一個小的 類型標籤(1-3 bits) 以及目前要儲存值的真實資料。類型標籤儲存在每個單元的低位元中,共有五種資料類型:###
000: object   - 当前存储的数据指向一个对象。
  1: int      - 当前存储的数据是一个 31 位的有符号整数。
010: double   - 当前存储的数据指向一个双精度的浮点数。
100: string   - 当前存储的数据指向一个字符串。
110: boolean  - 当前存储的数据是布尔值。

如果最低位是1,則類型標籤標誌位的長度只有一位;如果最低位是0,則類型標籤標誌位的長度佔三位,為儲存其他四種資料型別提供了額外兩個bit 的長度。

有兩種特殊資料型態:

  • undefined的值是(-2)30(一個超出整數範圍的數字);

  • null 的值是機器碼NULL 指標(null 指標的值全是0)

那也就是說null的型別標籤也是000,和Object的型別標籤一樣,所以會被判定為Object。

1.7 為什麼0.1 0.2 ! == 0.3,如何讓其相等(精度丟失)

計算機是透過二進位的方式儲存資料的,所以計算機計算0.1 0.2的時候,實際上是計算的兩個數的二進制的和。

在 Js中只有一種數字類型:Number,它的實作遵循IEEE 754標準,使用64位元固定長度來表示,也就是標準的double雙精度浮點數。在二元科學表示法中,雙精度浮點數的小數部分最多只能保留52位,再加上前面的1,其實就是保留53位有效數字,剩餘的需要捨去,遵從「0舍1入」的原則。

根據這個原則,0.1和0.2的二進位數相加,再轉換成十進位數就是:0.30000000000000004。所以不相等

解決方法就是設定一個誤差範圍,通常稱為「機器精度」。對JavaScript來說,這個值通常是2-52,在ES6中,提供了Number.EPSILON屬性,而它的值就是2-52,只要判斷0.1 0.2-0.3是否小於Number.EPSILON,如果小於,就可以判斷為0.1 0.2 ===0.3

function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

1.8 如何取得安全的undefined 值?

因為 undefined 是標識符,所以可以當作變數來使用和賦值,但是這樣會影響 undefined 的正常判斷。表達式 void ___ 沒有回傳值,因此傳回結果是 undefined。 void 不會改變表達式的結果,只是讓表達式不回傳值。因此可以用 void 0 來得到 undefined。

1.9 typeof NaN 的結果是什麼?

NaN 指「不是一個數字」(not a number),NaN 是一個「警戒值」(sentinel value,有特殊用途的常規值),用於指出數字類型中的錯誤情況,即「執行數學運算沒有成功,這是失敗後回傳的結果」。

typeof NaN; // "number"

NaN 是一個特殊值,它和自身不相等,是唯一非自反的值。所謂的非自反就是說,NaN 與誰都不相等,包括它本身,但在 NaN ! = NaN 下會回傳true

1.10 isNaN 和 Number.isNaN 函數的差別?

函數isNaN 接收參數後,會嘗試將這個參數轉換為數值,任何無法轉換為數值的的值都會回傳true,因此非數字值傳入也會回傳true ,會影響NaN 的判斷。

函數 Number.isNaN 會先判斷傳入參數是否為數字,如果是數字再繼續判斷是否為 NaN ,則不會進行資料類型的轉換,而這種方法對於 NaN 的判斷更為準確。

1.11 == 運算子的強制型別轉換規則是什麼?

對於 == 來說,如果對比雙方的型別不一樣,就會進行型別轉換。假如對比x 和y 是否相同,就會進行如下判斷流程:

首先會判斷兩者類型是否相同,相同的話就比較兩者的大小;

類型不相同的話,就會進行型別轉換;

會先判斷是否在對比null 和undefined,是的話就會回傳true

判斷兩者型別是否為string 和number,是的話就會將字符字串轉換為number

1 == &#39;1&#39;
      ↓
1 ==  1

判斷其中一方是否為boolean,是的話就會把boolean 轉為number 再進行判斷

&#39;1&#39; == true
        ↓
&#39;1&#39; ==  1
        ↓
 1  ==  1

判斷其中一方是否為object 且另一方為string、number或是symbol,是的話就會把object 轉為原始型別再進行判斷

&#39;1&#39; == { name: &#39;js&#39; }        ↓&#39;1&#39; == &#39;[object Object]&#39;

其流程圖如下:

JavaScript萬字面試總結

1.12其他值類型轉成字串的轉換規則?

Null 和Undefined 類型,null 轉換為"null",undefined 轉換為"undefined",

Boolean 類型,true 轉換為"true",false 轉換為"false"。

Number 類型的值直接轉換,不過那些極小且極大的數字會使用指數形式。

Symbol 類型的值直接轉換,但只允許明確強制型別轉換,使用隱式強制型別轉換會產生錯誤。

对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

1.13. 其他值类型转成数字的转换规则?

Undefined 类型的值转换为 NaN。

Null 类型的值转换为 0。

Boolean 类型的值,true 转换为 1,false 转换为 0。

String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。

Symbol 类型的值不能转换为数字,会报错。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值, 隐式转换会首先检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

1.14 其他值类型转成布尔类型的转换规则?

以下这些是假值: undefined 、 null 、 false 、 +0、-0 和 NaN 、 ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

1.15. || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。

  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

1.16. Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。

  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。

  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

1.17. 什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象。如:

const a = "abc";
a.length; // 3

在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。

1.18 Js中隐式转换规则

在 if 语句、逻辑语句、数学运算逻辑、== 等情况下都可能出现隐式类型转换。

JavaScript萬字面試總結

坑: 判断时, 尽量不要用 = = , 要用 = = = ( 两个等号判断, 如果类型不同, 默认会进行隐式类型转换再比较)

1.19 说说你对this的理解

this是一个在运行时才进行绑定的引用,在不同的情况下它可能会被绑定不同的对象。

1.20 如何判断 this 的指向

第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

this绑定的优先级

new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

1.21 Map和Object的区别

JavaScript萬字面試總結

1.22 说说你对JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。

JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

1.222 String和JSON.stringify的区别

console.log(String("abc")); // abc
console.log(JSON.stringify("abc")); // "abc"
console.log(String({ key: "value" })); // [object Object]
console.log(JSON.stringify({ key: "value" })); // {"key":"value"}
console.log(String([1, 2, 3])); // 1,2,3
console.log(JSON.stringify([1, 2, 3])); // [1,2,3]
const obj = {
    title: "devpoint",
    toString() {
        return "obj";
    },
};
console.log(String(obj)); // obj
console.log(JSON.stringify(obj)); // {"title":"devpoint"}

当需要将一个数组和一个普通对象转换为字符串时,经常使用JSON.stringify。

如果需要对象的toString方法被重写,则需要使用String()。

在其他情况下,使用String()将变量转换为字符串。

1.23 什么是伪数组(类数组)

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。

常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

1.24 类数组转换成数组的方法有哪些

常见的类数组转换为数组的方法有这样几种:

通过 call 调用数组的 slice 方法来实现转换

Array.prototype.slice.call(arrayLike);

通过 call 调用数组的 splice 方法来实现转换

Array.prototype.splice.call(arrayLike, 0);

通过 apply 调用数组的 concat 方法来实现转换

Array.prototype.concat.apply([], arrayLike);

通过 Array.from 方法来实现转换

Array.from(arrayLike);

1.25 Unicode、UTF-8、UTF-16、UTF-32的区别?

Unicode 是编码字符集(字符集),而UTF-8、UTF-16、UTF-32是字符集编码(编码规则);

UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;

UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;

如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;

1.26 常见的位运算符有哪些?其计算规则是什么?

现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。

常见的位运算有以下几种:

JavaScript萬字面試總結

1.27 为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

要遍历类数组,有三个方法:

(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法将类数组转化成数组:‌

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展开运算符将类数组转化成数组

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

1.28 escape、encodeURI、encodeURIComponent 的区别

encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。

encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。

escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

1.29 什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。

但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

1.30 use strict是什么? 它有什么用?

use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;

消除代码运行的不安全之处,保证代码运行的安全;

提高编译器效率,增加运行速度;

为未来新版本的 Javascript 做好铺垫。

区别:

禁止使用 with 语句。

禁止 this 关键字指向全局对象。

对象不能有重名的属性。

1.31 如何判断一个对象是否属于某个类?

第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。

第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。

1.32 强类型语言和弱类型语言的区别

强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java和C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。

弱型別語言:弱型別語言也稱為弱型別定義語言,與強型別定義相反。 JavaScript語言就屬於弱型別語言。簡單理解就是一種變數類型可以被忽略的語言。例如JavaScript是弱型別定義的,在JavaScript中就可以將字串'12'和整數3進行連接得到字串'123',在相加的時候會進行強制型別轉換。

兩者比較:強型別語言在速度上可能略遜色於弱型別語言,但是強型別語言帶來的嚴謹性可以有效地幫助避免許多錯誤。

1.33 解釋性語言和編譯型語言的差異

#(1)解釋型語言使用專門的解釋器對原始程式逐行解釋成特定平台的機器碼並立即執行。是程式碼在執行時才被解釋器一行行動態翻譯和執行,而不是在執行前完成翻譯。解釋型語言不需要事先編譯,其直接將原始碼解釋成機器碼並立即執行,所以只要某一平台提供了對應的解釋器即可運行該程式。其特點總結如下

解釋型語言每次運行都需要將原始程式碼解釋稱機器碼並執行,效率較低;

只要平台提供對應的解釋器,就可以運行來源程式碼,所以可以方便原始程式移植;

JavaScript、Python等屬於解釋型語言。

(2)編譯型語言使用專門的編譯器,針對特定的平台,將高階語言原始碼一次性的編譯成可被該平台硬體執行的機器碼,並包裝成該平台所能識別的可執行性程序的格式。在編譯型語言寫的程式執行之前,需要一個專門的編譯過程,把源代碼編譯成機器語言的文件,如exe格式的文件,以後要再運行時,直接使用編譯結果即可,如直接運行exe文件。因為只要編譯一次,以後執行時不需要編譯,所以編譯型語言執行效率高。其特點總結如下:

一次性的編譯成平台相關的機器語言文件,運行時脫離開發環境,運行效率高;

與特定平台相關,一般無法移植到其他平台;

C、C 等屬於編譯型語言。

兩者主要區別在於: 前者原始程式編譯後即可在該平台運行,後者是在運行期間才編譯。所以前者運行速度快,後者跨平台性佳。

1.34 for...in和for...of的區別

#for…of 是ES6新增的遍歷方式,允許遍歷一個含有iterator介面的資料結構(陣列、物件等)並且傳回各項的值,和ES3中的for…in的差異如下

for…of 遍歷取得的是物件的鍵值,for …in 取得的是物件的鍵名;

for… in 會遍歷物件的整個原型鏈,效能非常差不建議使用,而for … of 只遍歷目前物件不會遍歷原型鏈;

對於數組的遍歷,for…in 會傳回數組中所有可枚舉的屬性(包括原型鏈上可枚舉的屬性),for…of 只傳回數組的下標對應的屬性值;

總結: for...in 迴圈主要是為了遍歷對象而生,不適用於遍歷數組;for...of 迴圈可以用來遍歷數組、類別數組對象,字串、Set、Map 以及Generator對象。

1.35 ajax、axios、fetch的差異

(1)AJAX Ajax 即「AsynchronousJavascriptAndXML」(非同步JavaScript 和XML),指一種創建互動式網頁應用的網頁開發技術。它是一種在無需重新載入整個網頁的情況下,能夠更新部分網頁的技術。透過在後台與伺服器進行少量資料交換,Ajax 可以使網頁實現非同步更新。這意味著可以在不重新載入整個網頁的情況下,對網頁的某個部分進行更新。傳統的網頁(不使用 Ajax)如果需要更新內容,必須重載整個網頁頁面。其缺點如下:

  • 本身是針對MVC編程,不符合前端MVVM的浪潮

  • 基於原生XHR開發,XHR本身的架構不清晰

  • 不符合關注分離(Separation of Concerns)的原則

  • 設定和呼叫方式非常混亂,而且基於事件的非同步模型不友好。

(2)Fetch fetch號稱是AJAX的替代品,是在ES6出現的,使用了ES6中的promise物件。 Fetch是基於promise設計的。 Fetch的程式碼結構比起ajax簡單多。 fetch不是ajax的進一步封裝,而是原生js,沒有使用XMLHttpRequest物件。

fetch的優點:

  • 語法簡潔,更語義化

  • 基於標準Promise 實現,支援async/await

  • 更底層,提供的API豐富(request, response)

  • 脫離了XHR,是ES規格里新的實作方式

fetch的缺點:

  • fetch只對網路請求報錯,對400,500都當做成功的請求,伺服器回傳400,500 錯誤碼時並不會reject,只有網路錯誤這些導致請求不能完成時,fetch才會被reject。

  • fetch預設不會帶cookie,需要新增設定項: fetch(url, {credentials: 'include'})

  • fetch不支援abort,不支援逾時控制,使用setTimeout及Promise.reject的實現的超時控制並不能阻止請求過程繼續在後台運行,造成了流量的浪費

  • fetch沒有辦法原生監測請求的進度,而XHR可以

(3)Axios Axios 是基於Promise封裝的HTTP客戶端,其特性如下:

  • #瀏覽器端發起XMLHttpRequests請求

  • node端發起http請求

  • 支援Promise API

  • ##監聽請求和返回
  • 對請求和返回進行轉換
  • 取消請求
  • ##自動轉換json資料
  • 客戶端支援抵禦XSRF攻擊

1.36 陣列的遍歷方法有哪些

JavaScript萬字面試總結

1.37 forEach和map方法有什麼差別

這方法都是用來遍歷數組的,兩者區別如下:

forEach()方法會針對每一個元素執行提供的函數,對資料的操作會改變原始數組,該方法沒有傳回值;

map()方法不會改變原始數組的值,傳回一個新數組,新數組中的值為原始數組呼叫函數處理之後的值;

1.38 說你對淺拷貝和深拷貝的理解

淺拷貝

淺拷貝,指的是創建新的數據,這個數據有著原始數據屬性值的一份精確拷貝

如果屬性是基本型,拷貝的就是基本型別的值。如果屬性是參考類型,拷貝的就是記憶體位址

即淺拷貝是拷貝一層,深層的參考類型則共享記憶體位址

常見的淺拷貝:
  • Object.assign
  • Object.create
  • slice
  • ## concat()

  • 展開運算子

#深拷貝

#深拷貝開闢一個新的堆疊,兩個物件屬完成相同,但是對應兩個不同的位址,修改一個物件的屬性,不會改變另一個物件的屬性

常見的深拷貝方式有:

  • _.cloneDeep()

  • jQuery.extend()

  • JSON.stringify()

  • 手寫循環遞歸

1.39 JSON.stringify深拷貝的缺點

如果obj裡面有時間對象,則JSON.stringify後再JSON.parse的結果,時間將只是字串的形式,而不是物件的形式

如果obj裡面有RegExp,則列印出來是空物件

如果物件中有函數或undefined,則會直接被丟掉

如果json裡有物件是由建構函式產生的,則會丟掉物件的constructon

#1.40 知道lodash嗎?它有哪些常見的API ?

Lodash是一個一致性、模組化、高效能的 JavaScript 實用工具庫。

_.cloneDeep 深度拷貝

_.reject 依照條件移除某個元素。

_.drop(array, [n=1] ) 作用:將array 中的前n 個元素去掉,然後回傳剩餘的部分.

1.41 LHS和RHS 查詢

LHS (Left-hand Side) 和RHS (Right-hand Side) ,是在程式碼執行階段JS 引擎操作變數的兩種方式,二者差異就是對變數的查詢目的是變數賦值還是查詢。

LHS 可以理解為變數在賦值運算子(=)的左側,例如 a = 1,目前引擎對變數 a 尋找的目的是變數賦值。在這種情況下,引擎不關心變數 a 原始值是什麼,只管將值 1 賦給 a 變數。

RHS 可以理解為變數在賦值運算子(=)的右側,例如:console.log(a),其中引擎對變數a的尋找目的是查詢,它需要找到變數a 對應的實際值是什麼,然後才能將它列印出來。

1.42 includes 比 indexOf好在哪裡?

includes可以偵測NaN,indexOf無法偵測NaN,includes內部使用了Number.isNaN對NaN進行了配對

1.43 AMD 和CMD的區別?

JavaScript萬字面試總結

1.44 (a == 1 && a == 2 && a == 3) 有可能是 true 嗎?

#

方案一:重写toString()或valueOf()

let a = {  
    i: 1,  
    toString: function () {    
        return a.i++;  
    }
}
console.log(a == 1 && a == 2 && a == 3); // true

方案二:数组

数组的toString接口默认调用数组的join方法,重写join方法。定义a为数字,每次比较时就会调用 toString()方法,我们把数组的shift方法覆盖toString即可:

let a = [1,2,3];
a.toString = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

当然把toString改为valueOf也是一样效果:

let a = [1,2,3];
a. valueOf  = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

方案三:使用Object.defineProperty()

Object.defineProperty()用于定义对象中的属性,接收三个参数:object对象、对象中的属性,属性描述符。属性描述符中get:访问该属性时自动调用。

var  _a = 1;
Object.defineProperty(this,&#39;a&#39;,{  
    get:function(){    
        return _a++  
    }
})
console.log(a===1 && a===2 && a===3)//true

1.45 JS中的 MUL 函数

MUL表示数的简单乘法。在这种技术中,将一个值作为参数传递给一个函数,而该函数将返回另一个函数,将第二个值传递给该函数,然后重复继续。例如:xyz可以表示为

const mul = x => y => z => x * y * z
console.log(mul(1)(2)(3)) // 6

1.46 深度遍历广度遍历的区别?

对于算法来说 无非就是时间换空间 空间换时间

1、深度优先不需要记住所有的节点, 所以占用空间小, 而广度优先需要先记录所有的节点占用空间大

2、深度优先有回溯的操作(没有路走了需要回头)所以相对而言时间会长一点

3、深度优先采用的是堆栈的形式, 即先进后出

4、广度优先则采用的是队列的形式, 即先进先出

1.47 JS中的设计模式有哪些?

  • 单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。

  • 策略模式

定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。

  • 代理模式

为一个对象提供一个代用品或占位符,以便控制对它的访问。

  • 中介者模式

通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。

  • 装饰者模式

在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。

1.48 forEach如何跳出循环?

forEach是不能通过break或者return来实现跳出循环的,为什么呢?实现过forEach的同学应该都知道,forEach的的回调函数形成了一个作用域,在里面使用return并不会跳出,只会被当做continue

可以利用try catch

  function getItemById(arr, id) {
        var item = null;
        try {
            arr.forEach(function (curItem, i) {
                if (curItem.id == id) {
                    item = curItem;
                    throw Error();
                }
            })
        } catch (e) {
        }
        return item;
    }

1.49 JS中如何将页面重定向到另一个页面?

1、使用 location.href:window.location.href ="url"

2、使用 location.replace: window.location.replace("url");

1.50 移动端如何实现上拉加载,下拉刷新?

上拉加载

上拉加载的本质是页面触底,或者快要触底时的动作

判断页面触底我们需要先了解一下下面几个属性

scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值

clientHeight:它是一个定值,表示屏幕可视区域的高度;

scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长度(包括body元素自身的padding)

综上我们得出一个触底公式:

scrollTop + clientHeight >= scrollHeight

下拉刷新

下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作

关于下拉刷新的原生实现,主要分成三步:

监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;

监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置

1.51 JS 中的数组和函数在内存中是如何存储的?

JavaScript 中的陣列儲存大致需要分為兩種情況:

同種型別資料的陣列分配連續的記憶體空間

存在非同種型別資料的陣列使用哈希映射分配記憶體空間

溫馨提示:可以想像連續的記憶體空間只需要根據索引(指標)直接計算儲存位置即可。如果是雜湊映射那麼首先需要計算索引值,然後如果索引值有衝突的場景下還需要進行二次查找(需要知道雜湊的儲存方式)。

二、閉包與作用域

2.1 什麼是閉包?

官方說法:閉包就是指有權存取另一個函數作用域中的變數的函數。

MDN說法:閉包是一種特殊的物件。它由兩部分構成:函數,以及創建該函數的環境。環境由閉包創建時在作用域中的任何局部變數組成。

深度回答

瀏覽器在載入頁面會把程式碼放在堆疊記憶體( ECStack )中執行,函數進棧執行會產生一個私有上下文( EC ),此上下文能保護裡面的使用變數( AO )不受外界幹擾,並且如果當前執行上下文中的某些內容,被上下文以外的內容佔用,當前上下文不會出棧釋放,這樣可以保存裡面的變數和變數值,所以我認為閉包是一種保存和保護內部私有變數的機制。

2.2 閉包的作用

#閉包有兩個常用的用途;

閉包的第一個用途是使我們在函數外部能夠存取到函數內部的變數。透過使用閉包,可以透過在外部呼叫閉包函數,從而在外部存取函數內部的變量,可以使用這種方法來建立私有變數。

閉包的另一個用途是使已經運行結束的函數上下文中的變數物件繼續留在記憶體中,因為閉包函數保留了這個變數物件的引用,所以這個變數物件不會被回收。

2.3 閉包在專案中的引用場景,以及帶來的問題

在實際的專案中,會基於閉包把自己寫的模組內容包裹起來,這樣寫就可以保護自己的程式碼是私有的,防止和全域變數或其他的程式碼衝突,這一點是利用保護機制。

但不建議過多的使用閉包,因為使用不被釋放的上下文,是佔用堆疊記憶體空間的,過多的使用會導致導致記憶體洩漏。

解決閉包帶來的記憶體洩漏問題的方法是:使用完閉包函數後手動釋放。

2.4 閉包的使用場景

#return 回一個函數

函數作為參數

IIFE (自執行函數)

循環賦值

使用回呼函數就是在使用閉包

節流防抖

函數柯里化

2.5 閉包的執行程序

形成私有上下文

進棧執行

一系列操作

(1).  初始化作用域鏈(兩頭)

(2).  初始化this

(3).  初始化arguments

(4).  賦值形參考

(5).  變數提升

(6).  程式碼執行

遇到變數就先看是否是自己私有的,不是自己私有的按照作用域鏈上查找,如果不是上級的就繼續線上查找,,直到EC(G),變數的查找其實就是一個作用域鏈的拼接過程,拼接查詢的鏈式就是作用域鏈。

正常情況下,程式碼執行完成之後,私有上下文出棧被回收。但是遇到特殊情況,如果當前私有上下文執行完成之後中的某個東西被執行上下文以外的東西佔用,則當前私有上下文就不會出棧釋放,也就是形成了不被銷毀的上下文,閉包。

2.6 執行上下文的型別

(1)全域執行上下文

任何不在函數內部的都是全域執行上下文,它首先會建立一個全域的window對象,並且設定this的值等於這個全域對象,一個程式中只有一個全域執行上下文。

(2)函數執行上下文

當一個函數被呼叫時,就會為該函數建立一個新的執行上下文,函數的上下文可以有任意多個。

(3) eval函數執行上下文

執行在eval函數中的程式碼會有屬於他自己的執行上下文,不過eval函數不常使用,不做介紹。

2.7 執行上下文堆疊是什麼

#JavaScript引擎使用執行上下文堆疊來管理執行上下文

當JavaScript執行程式碼時,首先遇到全域程式碼,會建立一個全域執行上下文並且壓入執行堆疊中,每當遇到一個函數調用,就會為該函數建立一個新的執行上下文並壓入棧頂,引擎會執行位於執行上下文棧頂的函數,當函數執行完成之後,執行上下文從堆疊中彈出,繼續執行下一個上下文。當所有的程式碼都執行完畢之後,從堆疊中彈出全域執行上下文。

2.8 執行上下文的三個階段

建立階段→ 執行階段→ 回收階段

#建立階段

(1)this綁定

在全域執行上下文中,this指向全域物件(window物件)

在函數執行上下文中,this指向取決於函數如何呼叫。如果它被一個引用對象調用,那麼this 會被設定成那個對象​​,否則this 的值被設定為全域物件或undefined

(2)創建詞法環境元件

詞法環境是一種有標識符-變數映射的資料結構,標識符是指變數/函數名,變數是對實際物件或原始資料的引用。

詞法環境的內部有兩個元件:加粗樣式:環境記錄器:用來儲存變數個函數宣告的實際位置外部環境的參考:可以存取父級作用域

# (3)建立變數環境元件

變數環境也是詞法環境,其環境記錄器持有變數宣告語句在執行情境中所建立的綁定關係。

執行階段

在這個階段,執行變數賦值、程式碼執行

如果Javascript 引擎在原始程式碼中宣告的實際位置找不到變數的值,那麼將為其指派undefined 值

回收階段

執行上下文出棧等待虛擬機器回收執行上下文

2.9 談談你對作用域的理解

作用域可以視為一套規則,這套規則用來管理引擎如何在目前作用域以及巢狀的子作用域根據識別碼名稱進行變數查找。

簡單來說作用域就是變數的有效範圍。在一定的空間裡可以對變數資料進行讀寫操作,而這個空間就是變數的作用域。

(1)全域作用域

直接寫在script標籤的JS程式碼,都在全域作用域。在全域作用域下宣告的變數叫做全域變數(在區塊層級外部定義的變數)。

全域變數在全域的任何位置下都可以使用;全域作用域中無法存取局部作用域的中的變數。

全域作用域在頁面開啟的時候創建,在頁面關閉時銷毀。

所有window 物件的屬性擁有全域作用域

var和function指令宣告的全域變數和函數是window物件的屬性與方法

let指令、const指令、 class指令宣告的全域變量,不屬於window物件的屬性

(2)函數作用域(局部作用域)

呼叫函數時會建立函數作用域,函數執行完畢以後,作用域銷毀。每呼叫一次函數就會建立一個新的函數作用域,他們之間是相互獨立的。

在函數作用域中可以存取全域變量,在函數的外部無法存取函數內的變數。

當在函數作用域操作一個變數時,它會先在自身作用域中尋找,如果有就直接使用,如果沒有就向上一作用域中尋找,直到找到全域作用域,如果全域作用域中仍然沒有找到,則會報錯。

(3)區塊級作用域

ES6之前JavaScript採用的是函數作用域 詞法作用域,ES6引入了區塊級作用域。

任何一對花括號{}中的語句集都屬於一個區塊,在區塊中使用let和const宣告的變量,外部是存取不到的,這種作用域的規則就叫區塊級作用域。

透過var宣告的變數或非嚴格模式下建立的函數宣告沒有區塊層級作用域。

(4)詞法作用域

詞法作用域是靜態的作用域,無論函數在哪裡被調用,也無論它如何被調用,它的詞法作用域都只由函數被宣告時所處的位置決定。

編譯的詞法分析階段基本上能夠知道全部標識符在哪裡以及是如何聲明的,從而能夠預測在執行過中如何對它們進行查找。

換句話說,詞法作用域就是你在寫程式碼的時候就已經決定了變數的作用域。

2.10 什麼是作用域鏈

#當在js中使用一個變數的時候,首先js引擎會嘗試在目前作用域下去尋找該變量,如果沒找到,再到它的上層作用域尋找,以此類推直到找到該變量或是已經到了全局作用域,這樣的變量作用域訪問的鍊式結構, 被稱之為作用域鏈

深度回答

作用域鏈的本質上是一個指向變數物件的指標列表。變數物件是一個包含了執行環境中所有變數和函數的物件。作用域鏈的前端始終都是目前執行上下文的變數物件。全域執行上下文的變數物件(也就是全域物件)始終是作用域鏈的最後一個物件。

2.11 作用域鏈的作用

#作用域鏈的作用是保證對執行環境有權存取的所有變數和函數的有序訪問,透過作用域鏈,可以存取到外層環境的變數和函數。

2.12 作用域的常見應用場景

作用域的一個常見運用場景之一,就是 模組化。

由於 javascript 並未原生支援模組化導致了許多令人口吐芬芳的問題,例如全域作用域污染和變數名稱衝突,程式碼結構臃腫且復用性不高。在正式的模組化方案推出之前,開發者為了解決這類問題,想到了使用函數作用域來建立模組的方案。

2.13 說說Js中的預解析?

JS 引擎在運行一份程式碼的時候,會按照下面的步驟進行工作:

1.把變數的宣告提升到目前作用域的最前面,只會提升聲明,不會提升賦值

2.把函數的聲明提升到目前作用域的最前面,只會提升聲明,不會提升呼叫

3.先提升function,在提升var

2.14 變數提升與函數提升的差別?

變數提升

簡單來說就是在JavaScript 程式碼執行前引擎會先進行預編譯,預編譯期間會將變數宣告與函數宣告提升至其對應作用域的最頂端,函數內宣告的變數只會提升至該函數作用域最頂層,當函數內部定義的一個變數與外部相同時,那麼函數體內的這個變數就會被上升到最頂端。

函數提升

函數提升只會提升函數宣告式寫法,函數表達式的寫法不存在函數提升

函數提升的優先權大於變數提升的優先權,即函數提昇在變數提升之上

2.14 如何延長作用域鏈?

作用域鍊是可以延長的。

延長作用域鏈: 執行環境的型別只有兩種,全域和局部(函數)。但是有些語句可以在作用域鏈的前端暫時增加一個變數對象,該變數對象會在程式碼執行後移除。

具體來說就是執行這兩個語句時,作用域鏈都會得到加強

try - catch 語句的catch 區塊:會創建一個新的變數對象,包含的是被拋出的錯誤對象的聲明。

with 語句:with 語句會將指定的物件加入到作用域鏈中。

2.15 瀏覽器的垃圾回收機制

#(1)記憶體的生命週期

JS 環境中分配的記憶體, 一般有以下生命週期:

記憶體分配:當我們宣告變數、函數、物件的時候,系統會自動為他們分配記憶體

記憶體使用:即讀寫內存,也就是使用變數、函數等

記憶體回收:使用完畢,由垃圾回收自動回收不再使用的記憶體

全域變數一般不會回收, 一般局部變數的的值, 不用了, 會被自動回收掉

(2)垃圾回收的概念

垃圾回收:JavaScript程式碼運行時,需要分配記憶體空間來儲存變數和值。當變數不在參與運作時,就需要係統收回被佔用的記憶體空間,這就是垃圾回收。

回收機制:

Javascript 具有自動垃圾回收機制,會定期對那些不再使用的變數、物件所佔用的記憶體進行釋放,原理就是找到不再使用的變量,然後釋放掉其占用的記憶體。

JavaScript中存在兩種變數:局部變數和全域變數。全域變數的生命週期會持續要頁面卸載;而局部變數宣告在函數中,它的生命週期從函數執行開始,直到函數執行結束,在這個過程中,局部變數會在堆疊或堆疊中儲存它們的值,當函數執行結束後,這些局部變數不再被使用,它們所佔有的空間就會被釋放。

不過,當局部變數被外部函數使用時,其中一個情況就是閉包,在函數執行結束後,函數外部的變數依然指向函數內部的局部變量,此時局部變數依然在被使用,所以不會回收。

(3)垃圾回收的方式

  • #1.引用計數法

這個用的相對較少,IE所採用的引用計數演算法。引用計數就是追蹤記錄每個值被引用的次數。當宣告了一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就是1。相反,如果包含這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,表示這個變數已經沒有價值,因此,在機回收期下次再運行時,這個變數所佔有的記憶體空間就會被釋放出來。

這種方法會造成循環引用的問題:例如:obj1和obj2透過屬性進行相互引用,兩個物件的引用次數都是2。當使用循環計數時,由於函數執行完後,兩個物件都離開作用域,函數執行結束,obj1和obj2也將繼續存在,因此它們的引用次數永遠不會是0,就會引起循環引用。

  • 2.標記清除法

現代的瀏覽器已經不再使用引用計數演算法了。

現代瀏覽器通用的大多是基於標記清除演算法的某些改進演算法,整體想法都是一致的。

標記清除是瀏覽器常見的垃圾回收方式,當變數進入執行環境時,就標記這個變數“進入環境”,被標記為“進入環境”的變數是不能被回收的,因為他們正在被使用。當變數離開環境時,就會被標記為“離開環境”,被標記為“離開環境”的變數會被記憶體釋放。

垃圾收集器在運行的時候會為儲存在記憶體中的所有變數都加上標記。然後,它會去掉環境中的變數以及被環境中的變數所引用的標記。而在此之後再被加上標記的變數將被視為準備刪除的變量,原因是環境中的變數已經無法存取這些變數了。最後。垃圾收集器完成記憶體清除工作,銷毀那些標記的值,並回收他們所佔用的記憶體空間。

(4)如何減少垃圾回收

雖然瀏覽器可以進行垃圾自動回收,但是當程式碼比較複雜時,垃圾回收所帶來的代價比較大,所以應該盡量減少垃圾回收。

對數組進行最佳化: 在清空一個數組時,最簡單的方法就是給其賦值為[ ],但是同時會建立一個新的空對象,可以將數組的長度設為0,以此來達到清空數組的目的。

對object進行最佳化: 物件盡量重複使用,對於不再使用的對象,就將其設為null,盡快被回收。

對函數進行最佳化: 在循環中的函數表達式,如果可以重複使用,盡量放在函數的外部。

(5)記憶體洩漏是什麼

是指由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體

(6)哪些情況會導致記憶體洩漏

以下四種情況會造成記憶體的洩漏:

#意外的全域變數: 由於使用未宣告的變量,而意外的創建了一個全域變量,而使這個變數一直留在記憶體中無法被回收。

被遺忘的計時器或回呼函數: 設定了setInterval 計時器,而忘記取消它,如果循環函數有對外部變數的引用的話,那麼這個變數會被一直留在記憶體中,而無法被回收。

脫離 DOM 的引用: 取得一個 DOM 元素的引用,而後面這個元素被刪除,由於一直保留了對這個元素的引用,所以它也無法被回收。

閉包: 不合理的使用閉包,從而導致某些變數一直被留在記憶體當中。

三、函數與函數式程式設計

#3.1 什麼是函數式程式設計

函數式程式設計是一種"程式設計範式"(programming paradigm),一種編寫程式的方法論

#主要的程式設計範式有三種:命令式編程,宣告式程式設計和函數式程式設計

##相較於命令式編程,函數式程式設計更強調程式執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而非設計一個複雜的執行過程

3.2 函數式程式設計的優缺點

#優點

更好的管理狀態:因為它的宗旨是無狀態,或者說更少的狀態,能最大化的減少這些未知、優化程式碼、減少出錯情況

更簡單的複用:固定輸入->固定輸出,沒有其他外部變數影響,並且無副作用。當這樣程式碼重複使用時,完全不需要考慮它的內部實現和外部影響

更優雅的組合:往大的說,網頁是由各個元件組成的。往小的說,一個函數也可能是由多個小函數組成的。更強的複用性,帶來更強大的組合性

隱性好處。減少程式碼量,提高維護性

缺點

性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销

资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式

递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

3.3 什么是纯函数,它有什么优点

纯函数是对给定的输入返还相同输出的函数,并且要求你所有的数据都是不可变的,即纯函数=无状态+数据不可变

特性:

函数内部传入指定的值,就会返回确定唯一的值

不会造成超出作用域的变化,例如修改全局变量或引用传递的参数

优势:

使用纯函数,我们可以产生可测试的代码

不依赖外部环境计算,不会产生副作用,提高函数的复用性

可读性更强 ,函数不管是否是纯函数 都会有一个语义化的名称,更便于阅读

可以组装成复杂任务的可能性。符合模块化概念及单一职责原则

3.4 什么是组合函数 (compose)

在函数式编程中,有一个很重要的概念就是函数组合,实际上就是把处理的函数数据像管道一样连接起来,然后让数据穿过管道连接起来,得到最终的结果。

组合函数,其实大致思想就是将 多个函数组合成一个函数,c(b(a(a(1)))) 这种写法简写为 compose(c, b, a, a)(x) 。但是注意这里如果一个函数都没有传入,那就是传入的是什么就返回什么,并且函数的执行顺序是和传入的顺序相反的。

var compose = (...funcs) => {
  // funcs(数组):记录的是所有的函数
  // 这里其实也是利用了柯里化的思想,函数执行,生成一个闭包,预先把一些信息存储,供下级上下文使用
  return (x) => {
    var len = funcs.length;
    // 如果没有函数执行,直接返回结果
    if (len === 0) return x;
    if (len === 1) funcs[0](x);
    return funcs.reduceRight((res, func) => {
      return func(res);
    }, x);
  };
};
var resFn = compose(c, b, a, a);
resFn(1);

组合函数的思想,在很多框架中也被使用,例如:redux,实现效果来说是其实和上面的代码等价。

3.5 什么是惰性函数

惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了

惰性函数相当于有记忆的功能一样,当它已经判断了一遍的话,第二遍就不会再判断了。

比如现在要求写一个test函数,这个函数返回首次调用时的new Date().getTime(),注意是首次,而且不允许有全局变量的污染

//一般会这样实现
var test = (function () {
    var t = null;
    return function () {
        if (t) {
            return t;
        }
        t = new Date().getTime();
        return t;
    }
})();
// 用惰性函数实现
var test = function () {
    var t = new Date().getTime();
    test = function () {
        return t;
    }
    return test();
}
console.log(test());
console.log(test());
console.log(test());

3.6 什么是高阶函数

高阶函数是指使用其他函数作为参数、或者返回一个函数作为结果的函数。

3.7 说说你对函数柯里化的理解

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

函数柯里化的好处:

(1)参数复用:需要输入多个参数,最终只需输入一个,其余通过 arguments 来获取

(2)提前确认:避免重复去判断某一条件是否符合,不符合则 return 不再继续执行下面的操作

(3)延迟运行:避免重复的去执行程序,等真正需要结果的时候再执行

3.8 什么是箭头函数,有什么特征

使用 "箭头" ( => ) 来定义函数. 箭头函数相当于匿名函数, 并且简化了函数定义

箭头函数的特征:

箭头函数没有this, this指向定义箭头函数所处的外部环境

箭头函数的this永远不会变,call、apply、bind也无法改变

箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名

箭头函数没有原型prototype

箭头函数不能当做一个构造函数 因为 this 的指向问题

箭头函数没有 arguments 在箭头函数内部访问这个变量访问的是外部环境的arguments, 可以使用 ...代替

3.9 说说你对递归函数的理解

如果一个函数在内部调用自身本身,这个函数就是递归函数

其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解

一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回

优点:结构清晰、可读性强

缺点:效率低、调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。

3.10 什么是尾递归

尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。

在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出

这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误

3.11 函数传参,传递复杂数据类型和简单数据类型有什么区别

传递复杂数据类型传递的是引用的地址,修改会改变

简单数据类型传递的是具体的值,不会相互影响

/* let a = 8
    function fn(a) {
      a = 9
    }
    fn(a)
    console.log(a) // 8 */
    let a = { age: 8 }
    function fn(a) {
      a.age = 9
    }
    fn(a)
    console.log(a.age) // 9

3.12 函数声明与函数表达式的区别

函数声明: funtion开头,有函数提升

函数表达式: 不是funtion开头,没有函数提升

3.13 什么是函数缓存,如何实现?

概念

函数缓存,就是将函数运算过的结果进行缓存

本质上就是用空间(缓存存储)换时间(计算过程)

常用于缓存数据计算结果和缓存对象

如何实现

实现函数缓存主要依靠闭包、柯里化、高阶函数

应用场景

对于昂贵的函数调用,执行复杂计算的函数

对于具有有限且高度重复输入范围的函数

对于具有重复输入值的递归函数

对于纯函数,即每次使用特定输入调用时返回相同输出的函数

3.14 call、apply、bind三者的异同

  • 共同点 :

都可以改变this指向;

三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window

  • 不同点:

call 和 apply 会调用函数, 并且改变函数内部this指向.

call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递,且apply和call是一次性传入参数,而bind可以分为多次传入

bind是返回绑定this之后的函数

  • 应用场景

call 经常做继承.

apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值

bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指

【相关推荐:JavaScript视频教程web前端

以上是JavaScript萬字面試總結的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除