做者:Mark Ajavascript
譯者:前端小智前端
來源:devjava
點贊再看,養成習慣git
本文
GitHub
github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。github
因爲篇幅過長,我將此係列分紅上中下三篇,上、中篇:面試
看完這幾道 JavaScript 面試題,讓你與考官對答如流(中)編程
看完這幾道 JavaScript 面試題,讓你與考官對答如流(上)json
async/await
及其如何工做?%
模運算符的狀況下檢查一個數字是不是偶數?Iterator
是什麼,有什麼做用?Generator
函數是什麼,有什麼做用?async/await
及其如何工做?async/await
是 JS 中編寫異步或非阻塞代碼的新方法。它創建在Promises之上,讓異步代碼的可讀性和簡潔度都更高。api
async/await
是 JS 中編寫異步或非阻塞代碼的新方法。 它創建在Promises
之上,相對於 Promise 和回調,它的可讀性和簡潔度都更高。 可是,在使用此功能以前,咱們必須先學習Promises
的基礎知識,由於正如我以前所說,它是基於Promise
構建的,這意味着幕後使用仍然是Promise。數組
使用 Promise
function callApi() {
return fetch("url/to/api/endpoint")
.then(resp => resp.json())
.then(data => {
//do something with "data"
}).catch(err => {
//do something with "err"
});
}
複製代碼
使用async/await
在async/await
,咱們使用 tru/catch 語法來捕獲異常。
async function callApi() {
try {
const resp = await fetch("url/to/api/endpoint");
const data = await resp.json();
//do something with "data"
} catch (e) {
//do something with "err"
}
}
複製代碼
注意:使用 async
關鍵聲明函數會隱式返回一個Promise。
const giveMeOne = async () => 1;
giveMeOne()
.then((num) => {
console.log(num); // logs 1
});
複製代碼
注意:await
關鍵字只能在async function
中使用。在任何非async function的函數中使用await
關鍵字都會拋出錯誤。await
關鍵字在執行下一行代碼以前等待右側表達式(多是一個Promise)返回。
const giveMeOne = async () => 1;
function getOne() {
try {
const num = await giveMeOne();
console.log(num);
} catch (e) {
console.log(e);
}
}
// Uncaught SyntaxError: await is only valid in async function
async function getTwo() {
try {
const num1 = await giveMeOne(); // 這行會等待右側表達式執行完成
const num2 = await giveMeOne();
return num1 + num2;
} catch (e) {
console.log(e);
}
}
await getTwo(); // 2
複製代碼
展開運算符(spread)是三個點(...
),能夠將一個數組轉爲用逗號分隔的參數序列。說的通俗易懂點,有點像化骨綿掌,把一個大元素給打散成一個個單獨的小元素。
剩餘運算符也是用三個點(...
)表示,它的樣子看起來和展開操做符同樣,可是它是用於解構數組和對象。在某種程度上,剩餘元素和展開元素相反,展開元素會「展開」數組變成多個元素,剩餘元素會收集多個元素和「壓縮」成一個單一的元素。
function add(a, b) {
return a + b;
};
const nums = [5, 6];
const sum = add(...nums);
console.log(sum);
複製代碼
在本例中,咱們在調用add
函數時使用了展開操做符,對nums
數組進行展開。因此參數a
的值是5
,b
的值是6
,因此sum
是11
。
function add(...rest) {
return rest.reduce((total,current) => total + current);
};
console.log(add(1, 2)); // 3
console.log(add(1, 2, 3, 4, 5)); // 15
複製代碼
在本例中,咱們有一個add
函數,它接受任意數量的參數,並將它們所有相加,而後返回總數。
const [first, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2,3,4,5]
複製代碼
這裏,咱們使用剩餘操做符提取全部剩餘的數組值,並將它們放入除第一項以外的其餘數組中。
默認參數是在 JS 中定義默認變量的一種新方法,它在ES6或ECMAScript 2015版本中可用。
//ES5 Version
function add(a,b){
a = a || 0;
b = b || 0;
return a + b;
}
//ES6 Version
function add(a = 0, b = 0){
return a + b;
}
add(1); // returns 1
複製代碼
咱們還能夠在默認參數中使用解構。
function getFirst([first, ...rest] = [0, 1]) {
return first;
}
getFirst(); // 0
getFirst([10,20,30]); // 10
function getArr({ nums } = { nums: [1, 2, 3, 4] }){
return nums;
}
getArr(); // [1, 2, 3, 4]
getArr({nums:[5,4,3,2,1]}); // [5,4,3,2,1]
複製代碼
咱們還可使用先定義的參數再定義它們以後的參數。
function doSomethingWithValue(value = "Hello World", callback = () => { console.log(value) }) {
callback();
}
doSomethingWithValue(); //"Hello World"
複製代碼
咱們如今複習一下JS的數據類型,JS數據類型被分爲兩大類,基本類型和引用類型。
基本類型:Undefined
,Null
,Boolean
,Number
,String
,Symbol
,BigInt
引用類型:Object
,Array
,Date
,RegExp
等,說白了就是對象。
其中引用類型有方法和屬性,可是基本類型是沒有的,但咱們常常會看到下面的代碼:
let name = "marko";
console.log(typeof name); // "string"
console.log(name.toUpperCase()); // "MARKO"
複製代碼
name
類型是 string
,屬於基本類型,因此它沒有屬性和方法,可是在這個例子中,咱們調用了一個toUpperCase()
方法,它不會拋出錯誤,還返回了對象的變量值。
緣由是基本類型的值被臨時轉換或強制轉換爲對象,所以name
變量的行爲相似於對象。 除null
和undefined
以外的每一個基本類型都有本身包裝對象。也就是:String
,Number
,Boolean
,Symbol
和BigInt
。 在這種狀況下,name.toUpperCase()
在幕後看起來以下:
console.log(new String(name).toUpperCase()); // "MARKO"
複製代碼
在完成訪問屬性或調用方法以後,新建立的對象將當即被丟棄。
隱式強制轉換是一種將值轉換爲另外一種類型的方法,這個過程是自動完成的,無需咱們手動操做。
假設咱們下面有一個例子。
console.log(1 + '6'); // 16
console.log(false + true); // 1
console.log(6 * '2'); // 12
複製代碼
第一個console.log
語句結果爲16
。在其餘語言中,這會拋出編譯時錯誤,但在 JS 中,1
被轉換成字符串,而後與+運
算符鏈接。咱們沒有作任何事情,它是由 JS 自動完成。
第二個console.log
語句結果爲1
,JS 將false
轉換爲boolean
值爲 0
,,true
爲1
,所以結果爲1
。
第三個console.log
語句結果12
,它將'2'
轉換爲一個數字,而後乘以6 * 2
,結果是12。
而顯式強制是將值轉換爲另外一種類型的方法,咱們須要手動轉換。
console.log(1 + parseInt('6'));
複製代碼
在本例中,咱們使用parseInt
函數將'6'
轉換爲number
,而後使用+
運算符將1
和6
相加。
NaN
表示**「非數字」**是 JS 中的一個值,該值是將數字轉換或執行爲非數字值的運算結果,所以結果爲NaN
。
let a;
console.log(parseInt('abc')); // NaN
console.log(parseInt(null)); // NaN
console.log(parseInt(undefined)); // NaN
console.log(parseInt(++a)); // NaN
console.log(parseInt({} * 10)); // NaN
console.log(parseInt('abc' - 2)); // NaN
console.log(parseInt(0 / 0)); // NaN
console.log(parseInt('10a' * 10)); // NaN
複製代碼
JS 有一個內置的isNaN
方法,用於測試值是否爲isNaN值,可是這個函數有一個奇怪的行爲。
console.log(isNaN()); // true
console.log(isNaN(undefined)); // true
console.log(isNaN({})); // true
console.log(isNaN(String('a'))); // true
console.log(isNaN(() => { })); // true
複製代碼
全部這些console.log
語句都返回true
,即便咱們傳遞的值不是NaN
。
在ES6
中,建議使用Number.isNaN
方法,由於它確實會檢查該值(若是確實是NaN
),或者咱們可使本身的輔助函數檢查此問題,由於在 JS 中,NaN是惟一的值,它不等於本身。
function checkIfNaN(value) {
return value !== value;
}
複製代碼
咱們可使用Array.isArray
方法來檢查值是否爲數組。 當傳遞給它的參數是數組時,它返回true
,不然返回false
。
console.log(Array.isArray(5)); // false
console.log(Array.isArray("")); // false
console.log(Array.isArray()); // false
console.log(Array.isArray(null)); // false
console.log(Array.isArray({ length: 5 })); // false
console.log(Array.isArray([])); // true
複製代碼
若是環境不支持此方法,則可使用polyfill
實現。
function isArray(value){
return Object.prototype.toString.call(value) === "[object Array]"
}
複製代碼
固然還可使用傳統的方法:
let a = []
if (a instanceof Array) {
console.log('是數組')
} else {
console.log('非數組')
}
複製代碼
%
模運算符的狀況下檢查一個數字是不是偶數?咱們能夠對這個問題使用按位&
運算符,&
對其操做數進行運算,並將其視爲二進制值,而後執行與運算。
function isEven(num) {
if (num & 1) {
return false
} else {
return true
}
}
複製代碼
0
二進制數是 000
1
二進制數是 001
2
二進制數是 010
3
二進制數是 011
4
二進制數是 100
5
二進制數是 101
6
二進制數是 110
7
二進制數是 111
以此類推...
與運算的規則以下:
a | b | a & b |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
所以,當咱們執行console.log(5&1)
這個表達式時,結果爲1
。首先,&
運算符將兩個數字都轉換爲二進制,所以5
變爲101
,1
變爲001
。
而後,它使用按位懷運算符比較每一個位(0
和1
)。 101&001
,從表中能夠看出,若是a & b
爲1
,因此5&1
結果爲1
。
101 & 001 |
---|
101 |
001 |
001 |
1&0
,結果是0
。0&0
,結果是0
。1&1
,結果是1
。001
,對應的十進制數,即1
。由此咱們也能夠算出console.log(4 & 1)
結果爲0
。知道4
的最後一位是0
,而0 & 1
將是0
。若是你很難理解這一點,咱們可使用遞歸函數來解決此問題。
function isEven(num) {
if (num < 0 || num === 1) return false;
if (num == 0) return true;
return isEven(num - 2);
}
複製代碼
檢查對象中是否存在屬性有三種方法。
第一種使用 in
操做符號:
const o = {
"prop" : "bwahahah",
"prop2" : "hweasa"
};
console.log("prop" in o); // true
console.log("prop1" in o); // false
複製代碼
第二種使用 hasOwnProperty
方法,hasOwnProperty()
方法會返回一個布爾值,指示對象自身屬性中是否具備指定的屬性(也就是,是否有指定的鍵)。
console.log(o.hasOwnProperty("prop2")); // true
console.log(o.hasOwnProperty("prop1")); // false
複製代碼
第三種使用括號符號obj["prop"]
。若是屬性存在,它將返回該屬性的值,不然將返回undefined
。
console.log(o["prop"]); // "bwahahah"
console.log(o["prop1"]); // undefined
複製代碼
即異步的 JavaScript 和 XML,是一種用於建立快速動態網頁的技術,傳統的網頁(不使用 AJAX)若是須要更新內容,必需重載整個網頁面。使用AJAX則不須要加載更新整個網頁,實現部份內容更新
用到AJAX的技術:
使用對象字面量:
const o = {
name: "前端小智",
greeting() {
return `Hi, 我是${this.name}`;
}
};
o.greeting(); // "Hi, 我是前端小智"
複製代碼
使用構造函數:
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function () {
return `Hi, 我是${this.name}`;
}
const mark = new Person("前端小智");
mark.greeting(); // "Hi, 我是前端小智"
複製代碼
使用 Object.create 方法:
const n = {
greeting() {
return `Hi, 我是${this.name}`;
}
};
const o = Object.create(n);
o.name = "前端小智";
複製代碼
Object.freeze()
Object.freeze()
方法能夠凍結一個對象。一個被凍結的對象不再能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象後該對象的原型也不能被修改。freeze()
返回和傳入的參數相同的對象。
Object.seal()
Object.seal()方法封閉一個對象,阻止添加新屬性並將全部現有屬性標記爲不可配置。當前屬性的值只要可寫就能夠改變。
複製代碼
方法的相同點:
方法不一樣點:
Object.seal
方法生成的密封對象,若是屬性是可寫的,那麼能夠修改屬性值。 * Object.freeze
方法生成的凍結對象,屬性都是不可寫的,也就是屬性值沒法更改。in
運算符和 Object.hasOwnProperty
方法有什麼區別?hasOwnPropert方法
hasOwnPropert()
方法返回值是一個布爾值,指示對象自身屬性中是否具備指定的屬性,所以這個方法會忽略掉那些從原型鏈上繼承到的屬性。
看下面的例子:
Object.prototype.phone= '15345025546';
let obj = {
name: '前端小智',
age: '28'
}
console.log(obj.hasOwnProperty('phone')) // false
console.log(obj.hasOwnProperty('name')) // true
複製代碼
能夠看到,若是在函數原型上定義一個變量phone
,hasOwnProperty
方法會直接忽略掉。
in 運算符
若是指定的屬性在指定的對象或其原型鏈中,則in
運算符返回true
。
仍是用上面的例子來演示:
console.log('phone' in obj) // true
複製代碼
能夠看到in
運算符會檢查它或者其原型鏈是否包含具備指定名稱的屬性。
看下面的例子:
hoistedFunc();
notHoistedFunc();
function hoistedFunc(){
console.log("注意:我會被提高");
}
var notHoistedFunc = function(){
console.log("注意:我沒有被提高");
}
複製代碼
notHoistedFunc
調用拋出異常:Uncaught TypeError: notHoistedFunc is not a function
,而hoistedFunc
調用不會,由於hoistedFunc
會被提高到做用域的頂部,而notHoistedFunc
不會。
在 JS 中有4種方法能夠調用函數。
做爲函數調用——若是一個函數沒有做爲方法、構造函數、apply
、call
調用時,此時 this
指向的是 window
對象(非嚴格模式)
//Global Scope
function add(a,b){
console.log(this);
return a + b;
}
add(1,5); // 打印 "window" 對象和 6
const o = {
method(callback){
callback();
}
}
o.method(function (){
console.log(this); // 打印 "window" 對象
});
複製代碼
做爲方法調用——若是一個對象的屬性有一個函數的值,咱們就稱它爲方法。調用該方法時,該方法的this
值指向該對象。
const details = {
name : "Marko",
getName(){
return this.name;
}
}
details.getName(); // Marko
複製代碼
做爲構造函數的調用-若是在函數以前使用new
關鍵字調用了函數,則該函數稱爲構造函數
。構造函數裏面會默認建立一個空對象,並將this
指向該對象。
function Employee(name, position, yearHired) {
// 建立一個空對象 {}
// 而後將空對象分配給「this」關鍵字
// this = {};
this.name = name;
this.position = position;
this.yearHired = yearHired;
// 若是沒有指定 return ,這裏會默認返回 this
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
複製代碼
使用apply
和call
方法調用——若是咱們想顯式地指定一個函數的this
值,咱們可使用這些方法,這些方法對全部函數均可用。
const obj1 = {
result:0
};
const obj2 = {
result:0
};
function reduceAdd(){
let result = 0;
for(let i = 0, len = arguments.length; i < len; i++){
result += arguments[i];
}
this.result = result;
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // reduceAdd 函數中的 this 對象將是 obj1
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // reduceAdd 函數中的 this 對象將是 obj2
複製代碼
緩存是創建一個函數的過程,這個函數可以記住以前計算的結果或值。使用緩存函數是爲了不在最後一次使用相同參數的計算中已經執行的函數的計算。這節省了時間,但也有不利的一面,即咱們將消耗更多的內存來保存之前的結果。
function memoize(fn) {
const cache = {};
return function (param) {
if (cache[param]) {
console.log('cached');
return cache[param];
} else {
let result = fn(param);
cache[param] = result;
console.log(`not cached`);
return result;
}
}
}
const toUpper = (str ="")=> str.toUpperCase();
const toUpperMemoized = memoize(toUpper);
toUpperMemoized("abcdef");
toUpperMemoized("abcdef");
複製代碼
這個緩存函數適用於接受一個參數。 咱們須要改變下,讓它接受多個參數。
const slice = Array.prototype.slice;
function memoize(fn) {
const cache = {};
return (...args) => {
const params = slice.call(args);
console.log(params);
if (cache[params]) {
console.log('cached');
return cache[params];
} else {
let result = fn(...args);
cache[params] = result;
console.log(`not cached`);
return result;
}
}
}
const makeFullName = (fName, lName) => `${fName} ${lName}`;
const reduceAdd = (numbers, startingValue = 0) => numbers.reduce((total, cur) => total + cur, startingValue);
const memoizedMakeFullName = memoize(makeFullName);
const memoizedReduceAdd = memoize(reduceAdd);
memoizedMakeFullName("Marko", "Polo");
memoizedMakeFullName("Marko", "Polo");
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
複製代碼
typeof null == 'object'
老是返回true
,由於這是自 JS 誕生以來null
的實現。曾經有人提出將typeof null == 'object'
修改成typeof null == 'null'
,可是被拒絕了,由於這將致使更多的bug。
咱們可使用嚴格相等運算符===
來檢查值是否爲null
。
function isNull(value){
return value === null;
}
複製代碼
new
關鍵字與構造函數一塊兒使用以建立對象:
function Employee(name, position, yearHired) {
this.name = name;
this.position = position;
this.yearHired = yearHired;
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
複製代碼
new
關鍵字作了4
件事:
{}
this
值__proto__
指向構造函數的prototype
return
語句,則返回this
看下面事例:
function Person() { this.name = '前端小智' }
根據上面描述的,new Person()
作了:
var obj = {}
this
值:this = obj__proto__
指向構造函數的prototype
:this.__proto__ = Person().prototype
this
:return this
不該該使用箭頭函數一些狀況:
this/arguments
時,因爲箭頭函數自己不具備this/arguments
,所以它們取決於外部上下文this
即對象自己。const
和Object.freeze
是兩個徹底不一樣的概念。
const
聲明一個只讀的變量,一旦聲明,常量的值就不可改變:
const person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
person = animal; // ERROR "person" is read-only
複製代碼
Object.freeze
適用於值,更具體地說,適用於對象值,它使對象不可變,即不能更改其屬性。
let person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
Object.freeze(person);
person.name = "Lima"; //TypeError: Cannot assign to read only property 'name' of object
console.log(person);
複製代碼
若是我們想要確保對象被深凍結,就必須建立一個遞歸函數來凍結對象類型的每一個屬性:
沒有深凍結
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
Object.freeze(person);
person.profession.name = "doctor";
console.log(person); //output { name: 'Leonardo', profession: { name: 'doctor' } }
複製代碼
深凍結
function deepFreeze(object) {
let propNames = Object.getOwnPropertyNames(object);
for (let name of propNames) {
let value = object[name];
object[name] = value && typeof value === "object" ?
deepFreeze(value) : value;
}
return Object.freeze(object);
}
let person = {
name: "Leonardo",
profession: {
name: "developer"
}
};
deepFreeze(person);
person.profession.name = "doctor"; // TypeError: Cannot assign to read only property 'name' of object
複製代碼
Iterator
是什麼,有什麼做用?遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
Iterator
的做用有三個:
for...of
循環,Iterator 接口主要供for...of
消費。遍歷過程:
每一次調用next
方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含value
和done
兩個屬性的對象。其中,value
屬性是當前成員的值,done
屬性是一個布爾值,表示遍歷是否結束。
//obj就是可遍歷的,由於它遵循了Iterator標準,且包含[Symbol.iterator]方法,方法函數也符合標準的Iterator接口規範。
//obj.[Symbol.iterator]() 就是Iterator遍歷器
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
複製代碼
Generator
函數是什麼,有什麼做用?若是說 JavaScrip 是 ECMAScript 標準的一種具體實現、Iterator
遍歷器是Iterator
的具體實現,那麼Generator
函數能夠說是Iterator
接口的具體實現方式。
執行Generator
函數會返回一個遍歷器對象,每一次Generator
函數裏面的yield都至關一次遍歷器對象的next()
方法,而且能夠經過next(value)
方法傳入自定義的value
,來改變Generator
函數的行爲。
Generator
函數能夠經過配合Thunk 函數更輕鬆更優雅的實現異步編程和控制流管理。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
原文:
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。