之前準備面試的時候整理的,希望能帶給大家一點幫助~
目錄
js爲什麼是單線程,有什麼好處?
js最初被設計用在瀏覽器中,假如js是多線程的,當第一個線程上的js與第二個線程上的js同時對一個dom進行操作時,這個dom就不知道該執行哪個線程上的指令。
js異步加載的方式
- 使用async屬性
- 使用defer屬性
- onload時動態添加script標籤
Microtasks、Macrotasks(事件循環event loop、任務隊列task queues)
常見macro-task:整體的script、setTimeout、setInterval
常見micro-task:promise、process.nextTick
js通過事件循環實現異步,具體過程爲:
執行一個宏任務,過程中如果遇到微任務,就將其放到微任務的事件隊列
裏,當前宏任務執行完成後,會查看微任務的事件隊列
,並將裏面全部的微任務依次執行完。然後在執行一個宏任務,這樣一直循環下去。
原型和原型鏈
構造函數、原型、實例的關係
構造函數中有一個prototype指針指向原型,原型中也有一個constructor指針指向構造函數。實例中有一個內部屬性__proto__指向原型。構造函數和實例間通過原型產生聯繫,他們本身沒有直接的關聯。
new的基本原理(當let fun = new Fun()時發生了什麼?)
- 在構造函數內部聲明瞭一個對象
- 將構造函數的作用域賦給這個對象(
obj.__proto__ = Fun.prototype
) - 執行構造函數(給對象添加屬性)
- 返回這個對象
這裏同時附上實現new的代碼:
// new的內部機制
function Person() {
this.name = "I'm from Person!"
}
let person = new Person()
console.log(person);
// new
function myNew(Constructor) {
let obj = {};
obj.__proto__ = Constructor.prototype;
obj.name = "I'm new!";
return obj;
}
console.log(myNew(Person));
繼承
知道了構造函數、原型、實例三者之間的關係後,可以試想一下,當一個構造函數的原型等於另一個對象實例時,會發生什麼?對,此時就形成了一個鏈式連接,原型鏈。
可以結合這個圖理解一下:
我們在查找一個對象的屬性時,如果在對象本身身上沒有找到,js就會沿着這個對象的原型鏈繼續向上查找,直到找到這個屬性。那如果一直查到了最頂部(最頂部對象的原型爲null)都沒有找到,就說明這個對象上沒有這個屬性。因此我們可以用繼承來實現屬性和函數的共享。
繼承也就是用原型鏈實現的,關於繼承,常考es5和es6的繼承(es5中組合繼承,es6中extends)。具體在代碼中的實現如下:
ES5:
/**
*es5繼承 Student繼承Person 組合繼承
*/
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
Person.prototype.sayName = function() {
console.log(this.name)
}
function Student(name, gender, grade) {
Person.call(this, name, gender);
this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
const student = new Student('張三', '男', '大三');
student.sayName();
console.log(student);
ES6:
/**
* es6繼承 Student繼承Person
*/
class Person {
constructor(name, gender) {
this.name = name;
this.gender = gender;
}
sayName() {
console.log(this.name);
}
}
class Student extends Person {
constructor(name, gender, grade) {
super(name, gender);
this.grade = grade;
}
}
const student = new Student('張三', '男', '大三');
student.sayName();
console.log(student);
prototype和__proto__的關係
所有的對象都擁有__proto__屬性,它指向構造函數的prototype原型對象,最後指向Object.prototype(Object是一個原生函數,所有的對象都是Object的實例)。
所有的函數都同時擁有__proto__和prototype屬性,函數的__proto__指向自己的函數實現,函數的prototype是一個對象,所以函數的prototype也有__proto__屬性,指向Object.prototype。
Object.prototype.__proto__指向null(原型鏈的終點指向null)。
typeof和instanceof
typeof用於檢測基本類型,當他檢測引用類型array和object時,得到的都是“object”。因此,我們需要instanceof。
instanceof用於檢測引用類型,它可以區分出array和object。其內部是通過原型鏈來實現的,比如 arr1 instanceof Array ,他會在arr1的原型鏈上查找,這裏只查找一層,arr1.__proto__ == Array.prototype
,返回true。
具體的實現:
// instanceof的內部機制
function myInstanceof(obj, Constructor) {
while(obj.__proto__ !== null) {
if(obj.__proto__ == Constructor.prototype) break;
obj.__proto__ = obj.__proto__.__proto__;
}
if(obj.__proto__ == null) return false;
return true;
}
const arr = [1,3,2,1,4];
console.log(myInstanceof(arr, Array));
console.log(myInstanceof(arr, Object));
堆內存和棧內存
棧內存:存放基本數據類型String、Number、undefined、Boolean、null
堆內存:存放引用類型Object、Array、Function、Date、RegExp、包裝類型(Boolean、Number、String)
這個可以跟深拷貝聯繫一下~
什麼是事件委託(事件代理),事件委託有哪些優點?
事件委託就是將事件綁定到父元素上,根據事件的冒泡,當子元素處理事件時會自動觸發父元素的事件。通過判斷事件對象event的target可以找到時間實際發生的子元素。
優點:提高性能、動態監聽。提高性能是因爲減少了事件監聽的數量,動態監聽是指當增加一個子元素時,該子元素自動擁有父級元素上綁定的事件。
舉例:最經典的就是ul和li標籤的事件監聽,比如我們在添加事件時候,採用事件委託機制,不會在li標籤上直接添加,而是在ul父元素上添加。
作用域、作用域鏈
js沒有塊作用域,只有函數作用域。函數內部的函數可以訪問到外函數中的變量,他們都可以訪問到全局作用域中的變量,全局執行環境的變量對象始終是作用域鏈中的最後一個對象。
es6中的let、const可以達到塊級作用域的效果。
提升
js存在變量提升。變量提升包括函數聲明提升和變量聲明提升,函數聲明提升優先於變量聲明提升,函數表達式不提升。
要點就這些,具體的可以看我的博客:關於JavaScript中的聲明提升
閉包
什麼是閉包
一個持有外部環境變量的函數就是閉包
哪些地方用到了閉包
回調函數、私有屬性、柯里化
閉包的缺陷
內存泄漏,博客:關於JavaScript的內存泄漏
this指向
普通函數,this指向調用它的對象
箭頭函數,this指向上下文對象
當this指向全局對象時也可能引起內存泄漏。
bind、call和apply
相同點:都可以改變this指向
區別:
- bind不調用函數,返回一個新的函數,只有一個參數,指明this的指向
- call會直接調用函數,call只有一個參數,指明this的指向
- apply有兩個參數,第二個參數一般爲數組,apply將數組展開傳給函數
深拷貝和淺拷貝,實現深拷貝
淺拷貝和深拷貝都只針對於引用數據類型,淺拷貝只複製指向某個對象的指針,而不復制對象本身,新舊對象還是共享同一塊內存,所以當一個對象發生變化時,另一個對象隨之改變;
深拷貝會另外創造一個一模一樣的對象,新對象跟原對象不共享內存,修改新對象不會改到原對象;
區別:淺拷貝只複製對象的第一層屬性、深拷貝可以對對象的屬性進行遞歸複製;
具體的實現:
兩種方法 ~
第一種,遞歸實現:
/**
*遞歸實現對象深拷貝
*
* @param {Object || Array} source
* @returns
*/
function deepClone(source) {
if(typeof source !== "object") return source; // 淺拷貝
let target = source instanceof Array ? [] : {};
for(let key in source) {
// 數組索引,對象鍵值
target[key] = typeof source[key] === 'object' ? deepClone(source[key]) : source[key];
}
return target;
}
// 對象淺拷貝
let obj1 = {a:1, b:2};
let easyObj = obj1;
easyObj.a = 3;
console.log(obj1, easyObj);
// 對象深拷貝
let obj2 = {a:1, b:2, c: new Date()};
let deepObj = deepClone(obj2);
deepObj.a = 3;
console.log(obj2, deepObj);
// 數組淺拷貝
let arr1 = [1, 2, {a: 1}];
let easyArr = arr1;
easyArr[2].a = 2;
console.log(arr1, easyArr);
// 數組深拷貝
let arr2 = [1, 2, {a: 1}];
let deepArr = deepClone(arr2);
deepArr[2].a = 2;
console.log(arr2, deepArr);
第二種,用json的內置方法:
/**
*json實現對象深拷貝
*
* @param {Object || Array} source
* @returns
*/
function deepClone(source) {
return JSON.parse(JSON.stringify(source));
}
// 對象淺拷貝
let obj1 = {a:1, b:2};
let easyObj = obj1;
easyObj.a = 3;
console.log(obj1, easyObj);
// 對象深拷貝
let obj2 = {a:1, b:2};
let deepObj = deepClone(obj2);
deepObj.a = 3;
console.log(obj2, deepObj);
// 數組淺拷貝
let arr1 = [1, 2, {a: 1}];
let easyArr = arr1;
easyArr[2].a = 2;
console.log(arr1, easyArr);
// 數組深拷貝
let arr2 = [1, 2, {a: 1}];
let deepArr = deepClone(arr2);
deepArr[2].a = 2;
console.log(arr2, deepArr);
函數的防抖和節流
目的:防止在事件持續觸發的過程中頻繁執行函數。
防抖,指觸發事件後在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。防抖函數分爲非立即執行版和立即執行版。
/**
* @desc 函數防抖
* @param func 函數
* @param wait 延遲執行毫秒數
* @param immediate true 表立即執行,false 表非立即執行
*/
function debounce(func,wait,immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
節流,就是指連續觸發事件但是在 n 秒中只執行一次函數。節流會稀釋函數的執行頻率。
對於節流,一般有兩種方式可以實現,分別是時間戳版和定時器版。
/**
* @desc 函數節流
* @param func 函數
* @param wait 延遲執行毫秒數
* @param type 1 表時間戳版,2 表定時器版
*/
function throttle(func, wait ,type) {
if(type===1){
let previous = 0;
}else if(type===2){
let timeout;
}
return function() {
let context = this;
let args = arguments;
if(type===1){
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
冒泡和捕獲
冒泡是從內向外,捕獲是從外向內。
冒泡一般會在講事件委託的時候提到。
具體的可以看我博客:關於事件冒泡和事件捕獲
原生Ajax
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304>) {
console.log(xhr.responseText);
} else {
console.error('error:::', xhr.status);
}
}
};
xhr.open("get", "demo.txt", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
注意點:
- send()方法接收一個參數,作爲請求主體發送的數據。如果不需要發送數據最好傳入null,因爲參數對有些瀏覽器來說是必須的
- 必須在調用open()之前指定onreadystatechange事件處理程序才能確保跨瀏覽器 兼容性
- 要成功發送請求頭部信息,必須在調用open()方法之後且調用send()方法之前調用setRequestHeader()
模塊化
CommonJs
用於後端node和前端webpack
接口:module.exports和require
特點:
- 模塊輸出的是一個值的拷貝,模塊是運行時加載,同步加載
- CommonJS 模塊的頂層this指向當前模塊
AMD(Asynchronous Module Definition,異步模塊定義)
瀏覽器端模塊化開發的規範,require.js爲AMD規範的實現
接口:define、require、config
特點:異步加載,不阻塞頁面的加載,能並行加載多個模塊,但是不能按需加載,必須提前加載所需依賴
ES6 module
接口:import、export、export default
內嵌在網頁中的用法:
<script type="module">
import utils from "./utils.js";
// other code
</script>
此時不能用file協議,否則會報跨域的錯誤
特點:
- ES6 模塊之中,頂層的this指向undefined,即不應該在頂層代碼使用this
- 自動採用嚴格模式"use strict"。須遵循嚴格模式的要求
- ES6 模塊的設計思想是儘量的靜態化,編譯時加載”或者靜態加載,編譯時輸出接口
- ES6 模塊export、import命令可以出現在模塊的任何位置,但是必須處於模塊頂層。如果處於塊級作用域內,就會報錯
- ES6模塊輸出的是值的引用
Tree-Shaking
介紹:消除無用的代碼,減少js包的大小,從而減少頁面的加載時間。
原理:找到有用的代碼打包進去。依賴es6的module模塊,tree shaking會分析文件項目裏具體哪些代碼被引入了,哪些沒有引入,然後將真正引入的代碼打包進去,最後沒有使用到的代碼自然就不會存在了。