Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
者:Mark A
譯者:前端小智
來源:dev
IIFE或立即調用的函數表達式是在創建或聲明后將被調用或執行的函數。創建IIFE的語法是,將function (){}包裹在在括號()內,然后再用另一個括號()調用它,如:(function(){})()
(function(){
??...
}?());
(function?()?{
??...
})();
(function?named(params)?{
??...
})();
(()?=>?{
});
(function?(global)?{
??...
})(window);
const?utility?=?(function?()?{
??return?{
????...
??}
})
這些示例都是有效的IIFE。倒數第二個救命表明我們可以將參數傳遞給IIFE函數。最后一個示例表明,我們可以將IIFE的結果保存到變量中,以便稍后使用。
IIFE的一個主要作用是避免與全局作用域內的其他變量命名沖突或污染全局命名空間,來個例子。
<script?src="https://cdnurl.com/somelibrary.js"></script>
假設我們引入了一個omelibr.js的鏈接,它提供了一些我們在代碼中使用的全局函數,但是這個庫有兩個方法我們沒有使用:createGraph和drawGraph,因為這些方法都有bug。我們想實現自己的createGraph和drawGraph方法。
解決此問題的一種方法是直接覆蓋:
<script?src="https://cdnurl.com/somelibrary.js"></script>
<script>
???function?createGraph()?{
??????//?createGraph?logic?here
???}
???function?drawGraph()?{
??????//?drawGraph?logic?here
???}
</script>
當我們使用這個解決方案時,我們覆蓋了庫提供給我們的那兩個方法。
另一種方式是我們自己改名稱:
<script?src="https://cdnurl.com/somelibrary.js"></script>
<script>
???function?myCreateGraph()?{
??????//?createGraph?logic?here
???}
???function?myDrawGraph()?{
??????//?drawGraph?logic?here
???}
</script>
當我們使用這個解決方案時,我們把那些函數調用更改為新的函數名。
還有一種方法就是使用IIFE:
<script?src="https://cdnurl.com/somelibrary.js"></script>
<script>
???const?graphUtility?=?(function?()?{
??????function?createGraph()?{
?????????//?createGraph?logic?here
??????}
??????function?drawGraph()?{
?????????//?drawGraph?logic?here
??????}
??????return?{
?????????createGraph,
?????????drawGraph
??????}
???})
</script>
在此解決方案中,我們要聲明了graphUtility 變量,用來保存IIFE執行的結果,該函數返回一個包含兩個方法createGraph和drawGraph的對象。
IIFE 還可以用來解決一個常見的面試題:
var?li?=?document.querySelectorAll('.list-group?>?li');
for?(var?i?=?0,?len?=?li.length;?i?<?len;?i++)?{
???li[i].addEventListener('click',?function?(e)?{
??????console.log(i);
???})
假設我們有一個帶有list-group類的ul元素,它有5個li子元素。當我們單擊單個li元素時,打印對應的下標值。但在此外上述代碼不起作用,這里每次點擊 li 打印 i 的值都是5,這是由于閉包的原因。
閉包只是函數記住其當前作用域,父函數作用域和全局作用域的變量引用的能力。當我們在全局作用域內使用var關鍵字聲明變量時,就創建全局變量i。因此,當我們單擊li元素時,它將打印5,因為這是稍后在回調函數中引用它時i的值。
使用 IIFE 可以解決此問題:
var?li?=?document.querySelectorAll('.list-group?>?li');
for?(var?i?=?0,?len?=?li.length;?i?<?len;?i++)?{
???(function?(currentIndex)?{
??????li[currentIndex].addEventListener('click',?function?(e)?{
?????????console.log(currentIndex);
??????})
???})(i);
}
該解決方案之所以行的通,是因為IIFE會為每次迭代創建一個新的作用域,我們捕獲i的值并將其傳遞給currentIndex參數,因此調用IIFE時,每次迭代的currentIndex值都是不同的。
apply() 方法調用一個具有給定this值的函數,以及作為一個數組(或類似數組對象)提供的參數。
const?details?=?{
??message:?'Hello?World!'
};
function?getMessage(){
??return?this.message;
}
getMessage.apply(details);?//?'Hello?World!'
call()方法的作用和 apply() 方法類似,區別就是call()方法接受的是參數列表,而apply()方法接受的是一個參數數組。
const?person?=?{
??name:?"Marko?Polo"
};
function?greeting(greetingMessage)?{
??return?`${greetingMessage}?${this.name}`;
}
greeting.apply(person,?['Hello']);?//?"Hello?Marko?Polo!"
call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數。
const?details?=?{
??message:?'Hello?World!'
};
function?getMessage(){
??return?this.message;
}
getMessage.call(details);?//?'Hello?World!'
注意:該方法的語法和作用與 apply() 方法類似,只有一個區別,就是call() 方法接受的是一個參數列表,而 apply() 方法接受的是一個包含多個參數的數組。
const?person?=?{
??name:?"Marko?Polo"
};
function?greeting(greetingMessage)?{
??return?`${greetingMessage}?${this.name}`;
}
greeting.call(person,?'Hello');?//?"Hello?Marko?Polo!"
apply()方法可以在使用一個指定的 this 值和一個參數數組(或類數組對象)的前提下調用某個函數或方法。call()方法類似于apply(),不同之處僅僅是call()接受的參數是參數列表。
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]);?//?15
reduceAdd.call(obj2,?1,?2,?3,?4,?5);?//?15
bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的this 被指定為 bind() 的第一個參數,而其余參數將作為新函數的參數,供調用時使用。
import?React?from?'react';
class?MyComponent?extends?React.Component?{
?????constructor(props){
??????????super(props);?
??????????this.state?=?{
?????????????value?:?""
??????????}??
??????????this.handleChange?=?this.handleChange.bind(this);?
??????????//?將?“handleChange”?方法綁定到?“MyComponent”?組件
?????}
?????handleChange(e){
???????//do?something?amazing?here
?????}
?????render(){
????????return?(
??????????????<>
????????????????<input?type={this.props.type}
????????????????????????value={this.state.value}
?????????????????????onChange={this.handleChange}??????????????????????
??????????????????/>
??????????????</>
????????)
?????}
}
函數式編程(通常縮寫為FP)是通過編寫純函數,避免共享狀態、可變數據、副作用 來構建軟件的過程。數式編程是聲明式 的而不是命令式 的,應用程序的狀態是通過純函數流動的。與面向對象編程形成對比,面向對象中應用程序的狀態通常與對象中的方法共享和共處。
函數式編程是一種編程范式 ,這意味著它是一種基于一些基本的定義原則(如上所列)思考軟件構建的方式。當然,編程范示的其他示例也包括面向對象編程和過程編程。
函數式的代碼往往比命令式或面向對象的代碼更簡潔,更可預測,更容易測試 - 但如果不熟悉它以及與之相關的常見模式,函數式的代碼也可能看起來更密集雜亂,并且 相關文獻對新人來說是不好理解的。
JavaScript支持閉包和高階函數是函數式編程語言的特點。
高階函數只是將函數作為參數或返回值的函數。
function?higherOrderFunction(param,callback){
????return?callback(param);
}
在JavaScript中,函數不僅擁有一切傳統函數的使用方式(聲明和調用),而且可以做到像簡單值一樣賦值(var func = function(){})、傳參(function func(x,callback){callback();})、返回(function(){return function(){}}),這樣的函數也稱之為第一級函數(First-class Function)。不僅如此,JavaScript中的函數還充當了類的構造函數的作用,同時又是一個Function類的實例(instance)。這樣的多重身份讓JavaScript的函數變得非常重要。
map() 方法創建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數后返回的結果。
function?map(arr,?mapCallback)?{
??//?首先,檢查傳遞的參數是否正確。
??if?(!Array.isArray(arr)?||?!arr.length?||?typeof?mapCallback?!==?'function')?{?
????return?[];
??}?else?{
????let?result?=?[];
????//?每次調用此函數時,我們都會創建一個?result?數組
????//?因為我們不想改變原始數組。
????for?(let?i?=?0,?len?=?arr.length;?i?<?len;?i++)?{
??????result.push(mapCallback(arr[i],?i,?arr));?
??????//?將?mapCallback?返回的結果?push?到?result?數組中
????}
????return?result;
??}
}
filter() 方法創建一個新數組, 其包含通過所提供函數實現的測試的所有元素。
function?filter(arr,?filterCallback)?{
??//?首先,檢查傳遞的參數是否正確。
??if?(!Array.isArray(arr)?||?!arr.length?||?typeof?filterCallback?!==?'function')?
??{
????return?[];
??}?else?{
????let?result?=?[];
?????//?每次調用此函數時,我們都會創建一個?result?數組
?????//?因為我們不想改變原始數組。
????for?(let?i?=?0,?len?=?arr.length;?i?<?len;?i++)?{
??????//?檢查?filterCallback?的返回值是否是真值
??????if?(filterCallback(arr[i],?i,?arr))?{?
??????//?如果條件為真,則將數組元素?push?到?result?中
????????result.push(arr[i]);
??????}
????}
????return?result;?//?return?the?result?array
??}
}
reduce() 方法對數組中的每個元素執行一個由您提供的reducer函數(升序執行),將其結果匯總為單個返回值。
function?reduce(arr,?reduceCallback,?initialValue)?{
??//?首先,檢查傳遞的參數是否正確。
??if?(!Array.isArray(arr)?||?!arr.length?||?typeof?reduceCallback?!==?'function')?
??{
????return?[];
??}?else?{
????//?如果沒有將initialValue傳遞給該函數,我們將使用第一個數組項作為initialValue
????let?hasInitialValue?=?initialValue?!==?undefined;
????let?value?=?hasInitialValue???initialValue?:?arr[0];
???、
????//?如果有傳遞?initialValue,則索引從?1?開始,否則從?0?開始
????for?(let?i?=?hasInitialValue???0?:?1,?len?=?arr.length;?i?<?len;?i++)?{
??????value?=?reduceCallback(value,?arr[i],?i,?arr);?
????}
????return?value;
??}
}
arguments對象是函數中傳遞的參數值的集合。它是一個類似數組的對象,因為它有一個length屬性,我們可以使用數組索引表示法arguments[1]來訪問單個值,但它沒有數組中的內置方法,如:forEach、reduce、filter和map。
我們可以使用Array.prototype.slice將arguments對象轉換成一個數組。
function?one()?{
??return?Array.prototype.slice.call(arguments);
}
注意:箭頭函數中沒有arguments對象。
function?one()?{
??return?arguments;
}
const?two?=?function?()?{
??return?arguments;
}
const?three?=?function?three()?{
??return?arguments;
}
const?four?=?()?=>?arguments;
four();?//?Throws?an?error??-?arguments?is?not?defined
當我們調用函數four時,它會拋出一個ReferenceError: arguments is not defined error。使用rest語法,可以解決這個問題。
const?four?=?(...args)?=>?args;
這會自動將所有參數值放入數組中。
我們可以使用Object.create方法創建沒有原型的對象。
const?o1?=?{};
console.log(o1.toString());?//?[object?Object]
const?o2?=?Object.create(null);
console.log(o2.toString());
//?throws?an?error?o2.toString?is?not?a?function?
function?myFunc()?{
??let?a?=?b?=?0;
}
myFunc();
原因是賦值運算符是從右到左的求值的。這意味著當多個賦值運算符出現在一個表達式中時,它們是從右向左求值的。所以上面代碼變成了這樣:
function?myFunc()?{
??let?a?=?(b?=?0);
}
myFunc();
首先,表達式b = 0求值,在本例中b沒有聲明。因此,JS引擎在這個函數外創建了一個全局變量b,之后表達式b = 0的返回值為0,并賦給新的局部變量a。
我們可以通過在賦值之前先聲明變量來解決這個問題。
function?myFunc()?{
??let?a,b;
??a?=?b?=?0;
}
myFunc();
ECMAScript 是編寫腳本語言的標準,這意味著JavaScript遵循ECMAScript標準中的規范變化,因為它是JavaScript的藍圖。
ECMAScript 和 Javascript,本質上都跟一門語言有關,一個是語言本身的名字,一個是語言的約束條件
只不過發明JavaScript的那個人(Netscape公司),把東西交給了ECMA(European Computer Manufacturers Association),這個人規定一下他的標準,因為當時有java語言了,又想強調這個東西是讓ECMA這個人定的規則,所以就這樣一個神奇的東西誕生了,這個東西的名稱就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自認為是一種廣義的JavaScript)
ECMAScript說什么JavaScript就得做什么!
JavaScript(狹義的JavaScript)做什么都要問問ECMAScript我能不能這樣干!如果不能我就錯了!能我就是對的!
——突然感覺JavaScript好沒有尊嚴,為啥要搞個人出來約束自己,
那個人被創造出來也好委屈,自己被創造出來完全是因為要約束JavaScript。
var聲明的變量會掛載在window上,而let和const聲明的變量不會:
var?a?=?100;
console.log(a,window.a);????//?100?100
let?b?=?10;
console.log(b,window.b);????//?10?undefined
const?c?=?1;
console.log(c,window.c);????//?1?undefined
var聲明變量存在變量提升,let和const不存在變量提升:
console.log(a);?//?undefined??===>??a已聲明還沒賦值,默認得到undefined值
var?a?=?100;
console.log(b);?//?報錯:b is?not?defined??===>?找不到b這個變量
let?b?=?10;
console.log(c);?//?報錯:c is?not?defined??===>?找不到c這個變量
const?c?=?10;
let和const聲明形成塊作用域
if(1){
??var?a?=?100;
??let?b?=?10;
}
console.log(a);?//?100
console.log(b)??//?報錯:b is not defined ?===>?找不到b這個變量
-------------------------------------------------------------
if(1){
??var?a?=?100;
??const?c?=?1;
}
console.log(a);?//?100
console.log(c)??//?報錯:c is not defined ?===>?找不到c這個變量
同一作用域下let和const不能聲明同名變量,而var可以
var?a?=?100;
console.log(a);?//?100
var?a?=?10;
console.log(a);?//?10
-------------------------------------
let?a?=?100;
let?a?=?10;
//??控制臺報錯:Identifier 'a' has already been declared ?===>?標識符a已經被聲明了。
暫存死區
var?a?=?100;
if(1){
????a?=?10;
????//在當前塊作用域中存在a使用let/const聲明的情況下,給a賦值10時,只會在當前作用域找變量a,
????//?而這時,還未到聲明時候,所以控制臺Error:a?is?not?defined
????let?a?=?1;
}
const
/*
*? 1、一旦聲明必須賦值,不能使用null占位。
*
*? 2、聲明后不能再修改
*
*? 3、如果聲明的是復合類型數據,可以修改其屬性
*
*?*/
const?a?=?100;?
const?list?=?[];
list[0]?=?10;
console.log(list); //?[10]
const?obj?=?{a:100};
obj.name?=?'apple';
obj.a?=?10000;
console.log(obj); //?{a:10000,name:'apple'}
箭頭函數表達式的語法比函數表達式更簡潔,并且沒有自己的this,arguments,super或new.target。箭頭函數表達式更適用于那些本來需要匿名函數的地方,并且它不能用作構造函數。
//ES5?Version
var?getCurrentDate?=?function?(){
??return?new?Date();
}
//ES6?Version
const?getCurrentDate?=?()?=>?new?Date();
在本例中,ES5 版本中有function(){}聲明和return關鍵字,這兩個關鍵字分別是創建函數和返回值所需要的。在箭頭函數版本中,我們只需要()括號,不需要 return 語句,因為如果我們只有一個表達式或值需要返回,箭頭函數就會有一個隱式的返回。
//ES5?Version
function?greet(name)?{
??return?'Hello?'?+?name?+?'!';
}
//ES6?Version
const?greet?=?(name)?=>?`Hello?${name}`;
const?greet2?=?name?=>?`Hello?${name}`;
我們還可以在箭頭函數中使用與函數表達式和函數聲明相同的參數。如果我們在一個箭頭函數中有一個參數,則可以省略括號。
const?getArgs?=?()?=>?arguments
const?getArgs2?=?(...rest)?=>?rest
箭頭函數不能訪問arguments對象。所以調用第一個getArgs函數會拋出一個錯誤。相反,我們可以使用rest參數來獲得在箭頭函數中傳遞的所有參數。
const?data?=?{
??result:?0,
??nums:?[1,?2,?3,?4,?5],
??computeResult()?{
????//?這里的“this”指的是“data”對象
????const?addAll?=?()?=>?{
??????return?this.nums.reduce((total,?cur)?=>?total?+?cur,?0)
????};
????this.result?=?addAll();
??}
};
箭頭函數沒有自己的this值。它捕獲詞法作用域函數的this值,在此示例中,addAll函數將復制computeResult 方法中的this值,如果我們在全局作用域聲明箭頭函數,則this值為 window 對象。
類(class)是在 JS 中編寫構造函數的新方法。它是使用構造函數的語法糖,在底層中使用仍然是原型和基于原型的繼承。
???//ES5?Version
???function?Person(firstName,?lastName,?age,?address){
??????this.firstName?=?firstName;
??????this.lastName?=?lastName;
??????this.age?=?age;
??????this.address?=?address;
???}
???Person.self?=?function(){
?????return?this;
???}
???Person.prototype.toString?=?function(){
?????return?"[object?Person]";
???}
???Person.prototype.getFullName?=?function?(){
?????return?this.firstName?+?"?"?+?this.lastName;
???}??
???//ES6?Version
???class?Person?{
????????constructor(firstName,?lastName,?age,?address){
????????????this.lastName?=?lastName;
????????????this.firstName?=?firstName;
????????????this.age?=?age;
????????????this.address?=?address;
????????}
????????static?self()?{
???????????return?this;
????????}
????????toString(){
???????????return?"[object?Person]";
????????}
????????getFullName(){
???????????return?`${this.firstName}?${this.lastName}`;
????????}
???}
重寫方法并從另一個類繼承。
//ES5?Version
Employee.prototype?=?Object.create(Person.prototype);
function?Employee(firstName,?lastName,?age,?address,?jobTitle,?yearStarted)?{
??Person.call(this,?firstName,?lastName,?age,?address);
??this.jobTitle?=?jobTitle;
??this.yearStarted?=?yearStarted;
}
Employee.prototype.describe?=?function?()?{
??return?`I?am?${this.getFullName()}?and?I?have?a?position?of?${this.jobTitle}?and?I?started?at?${this.yearStarted}`;
}
Employee.prototype.toString?=?function?()?{
??return?"[object?Employee]";
}
//ES6?Version
class?Employee?extends?Person?{?//Inherits?from?"Person"?class
??constructor(firstName,?lastName,?age,?address,?jobTitle,?yearStarted)?{
????super(firstName,?lastName,?age,?address);
????this.jobTitle?=?jobTitle;
????this.yearStarted?=?yearStarted;
??}
??describe()?{
????return?`I?am?${this.getFullName()}?and?I?have?a?position?of?${this.jobTitle}?and?I?started?at?${this.yearStarted}`;
??}
??toString()?{?//?Overriding?the?"toString"?method?of?"Person"
????return?"[object?Employee]";
??}
}
所以我們要怎么知道它在內部使用原型?
class?Something?{
}
function?AnotherSomething(){
}
const?as?=?new?AnotherSomething();
const?s?=?new?Something();
console.log(typeof?Something);?//?"function"
console.log(typeof?AnotherSomething);?//?"function"
console.log(as.toString());?//?"[object?Object]"
console.log(as.toString());?//?"[object?Object]"
console.log(as.toString?===?Object.prototype.toString);?//?true
console.log(s.toString?===?Object.prototype.toString);?//?true
模板字符串是在 JS 中創建字符串的一種新方法。我們可以通過使用反引號使模板字符串化。
//ES5?Version
var?greet?=?'Hi?I\'m?Mark';
//ES6?Version
let?greet?=?`Hi?I'm?Mark`;
在 ES5 中我們需要使用一些轉義字符來達到多行的效果,在模板字符串不需要這么麻煩:
//ES5?Version
var?lastWords?=?'\n'
??+?'???I??\n'
??+?'???Am??\n'
??+?'Iron?Man?\n';
//ES6?Version
let?lastWords?=?`
????I
????Am
??Iron?Man???
`;
在ES5版本中,我們需要添加\n以在字符串中添加新行。在模板字符串中,我們不需要這樣做。
//ES5?Version
function?greet(name)?{
??return?'Hello?'?+?name?+?'!';
}
//ES6?Version
function?greet(name)?{
??return?`Hello?${name}?!`;
}
在 ES5 版本中,如果需要在字符串中添加表達式或值,則需要使用+運算符。在模板字符串s中,我們可以使用${expr}嵌入一個表達式,這使其比 ES5 版本更整潔。
對象析構是從對象或數組中獲取或提取值的一種新的、更簡潔的方法。假設有如下的對象:
const?employee?=?{
??firstName:?"Marko",
??lastName:?"Polo",
??position:?"Software?Developer",
??yearHired:?2017
};
從對象獲取屬性,早期方法是創建一個與對象屬性同名的變量。這種方法很麻煩,因為我們要為每個屬性創建一個新變量。假設我們有一個大對象,它有很多屬性和方法,用這種方法提取屬性會很麻煩。
var?firstName?=?employee.firstName;
var?lastName?=?employee.lastName;
var?position?=?employee.position;
var?yearHired?=?employee.yearHired;
使用解構方式語法就變得簡潔多了:
{?firstName,?lastName,?position,?yearHired?}?=?employee;
我們還可以為屬性取別名:
let?{?firstName:?fName,?lastName:?lName,?position,?yearHired?}?=?employee;
當然如果屬性值為 undefined 時,我們還可以指定默認值:
let?{?firstName?=?"Mark",?lastName:?lName,?position,?yearHired?}?=?employee;
模塊使我們能夠將代碼基礎分割成多個文件,以獲得更高的可維護性,并且避免將所有代碼放在一個大文件中。在 ES6 支持模塊之前,有兩個流行的模塊。
基本上,使用模塊的方式很簡單,import用于從另一個文件中獲取功能或幾個功能或值,同時export用于從文件中公開功能或幾個功能或值。
導出
使用 ES5 (CommonJS)
//?使用?ES5?CommonJS?-?helpers.js
exports.isNull?=?function?(val)?{
??return?val?===?null;
}
exports.isUndefined?=?function?(val)?{
??return?val?===?undefined;
}
exports.isNullOrUndefined?=?function?(val)?{
??return?exports.isNull(val)?||?exports.isUndefined(val);
}
使用 ES6 模塊
//?使用?ES6?Modules?-?helpers.js
export?function?isNull(val){
??return?val?===?null;
}
export?function?isUndefined(val)?{
??return?val?===?undefined;
}
export?function?isNullOrUndefined(val)?{
??return?isNull(val)?||?isUndefined(val);
}
在另一個文件中導入函數
//?使用?ES5?(CommonJS)?-?index.js
const?helpers?=?require('./helpers.js');?//?helpers?is?an?object
const?isNull?=?helpers.isNull;
const?isUndefined?=?helpers.isUndefined;
const?isNullOrUndefined?=?helpers.isNullOrUndefined;
//?or?if?your?environment?supports?Destructuring
const?{?isNull,?isUndefined,?isNullOrUndefined?}?=?require('./helpers.js');
-------------------------------------------------------
//?ES6?Modules?-?index.js
import?*?as?helpers?from?'./helpers.js';?//?helpers?is?an?object
//?or?
import?{?isNull,?isUndefined,?isNullOrUndefined?as?isValid?}?from?'./helpers.js';
//?using?"as"?for?renaming?named?exports
在文件中導出單個功能或默認導出
使用 ES5 (CommonJS)
//?使用?ES5?(CommonJS)?-?index.js
class?Helpers?{
??static?isNull(val)?{
????return?val?===?null;
??}
??static?isUndefined(val)?{
????return?val?===?undefined;
??}
??static?isNullOrUndefined(val)?{
????return?this.isNull(val)?||?this.isUndefined(val);
??}
}
module.exports?=?Helpers;
使用ES6 Modules
//?使用?ES6?Modules?-?helpers.js
class?Helpers?{
??static?isNull(val)?{
????return?val?===?null;
??}
??static?isUndefined(val)?{
????return?val?===?undefined;
??}
??static?isNullOrUndefined(val)?{
????return?this.isNull(val)?||?this.isUndefined(val);
??}
}
export?default?Helpers
從另一個文件導入單個功能
使用ES5 (CommonJS)
//?使用?ES5?(CommonJS)?-?index.js
const?Helpers?=?require('./helpers.js');?
console.log(Helpers.isNull(null));
使用 ES6 Modules
import?Helpers?from?'.helpers.js'
console.log(Helpers.isNull(null));??????????
Set 對象允許你存儲任何類型的唯一值,無論是原始值或者是對象引用。
我們可以使用Set構造函數創建Set實例。
const?set1?=?new?Set();
const?set2?=?new?Set(["a","b","c","d","d","e"]);
我們可以使用add方法向Set實例中添加一個新值,因為add方法返回Set對象,所以我們可以以鏈式的方式再次使用add。如果一個值已經存在于Set對象中,那么它將不再被添加。
set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
//?后一個“k”不會被添加到set對象中,因為它已經存在了
我們可以使用has方法檢查Set實例中是否存在特定的值。
set2.has("a")?//?true
set2.has("z")?//?true
我們可以使用size屬性獲得Set實例的長度。
set2.size?//?returns?10
可以使用clear方法刪除 Set 中的數據。
set2.clear();
我們可以使用Set對象來刪除數組中重復的元素。
const?numbers?=?[1,?2,?3,?4,?5,?6,?6,?7,?8,?8,?5];
const?uniqueNums?=?[...new?Set(numbers)];?//?[1,2,3,4,5,6,7,8]
回調函數是一段可執行的代碼段,它作為一個參數傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數)代碼。
在JavaScript中函數也是對象的一種,同樣對象可以作為參數傳遞給函數,因此函數也可以作為參數傳遞給另外一個函數,這個作為參數的函數就是回調函數。
const?btnAdd?=?document.getElementById('btnAdd');
btnAdd.addEventListener('click',?function?clickCallback(e)?{
????//?do?something?useless
});
在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執行clickCallback函數。回調函數向某些數據或事件添加一些功能。
數組中的reduce、filter和map方法需要一個回調作為參數。回調的一個很好的類比是,當你打電話給某人,如果他們不接,你留下一條消息,你期待他們回調。調用某人或留下消息的行為是事件或數據,回調是你希望稍后發生的操作。
Promise 是異步編程的一種解決方案:從語法上講,promise是一個對象,從它可以獲取異步操作的消息;從本意上講,它是承諾,承諾它過一段時間會給你一個結果。promise有三種狀態:pending(等待態),fulfiled(成功態),rejected(失敗態);狀態一旦改變,就不會再變。創造promise實例后,它會立即執行。
fs.readFile('somefile.txt',?function?(e,?data)?{
??if?(e)?{
????console.log(e);
??}
??console.log(data);
});
如果我們在回調內部有另一個異步操作,則此方法存在問題。我們將有一個混亂且不可讀的代碼。此代碼稱為“回調地獄”。
//?回調地獄
fs.readFile('somefile.txt',?function?(e,?data)?{
??//your?code?here
??fs.readdir('directory',?function?(e,?files)?{
????//your?code?here
????fs.mkdir('directory',?function?(e)?{
??????//your?code?here
????})
??})
})
如果我們在這段代碼中使用promise,它將更易于閱讀、理解和維護。
promReadFile('file/path')
??.then(data?=>?{
????return?promReaddir('directory');
??})
??.then(data?=>?{
????return?promMkdir('directory');
??})
??.catch(e?=>?{
????console.log(e);
??})
promise有三種不同的狀態:
pending 狀態的 Promise 對象會觸發 fulfilled/rejected 狀態,在其狀態處理方法中可以傳入參數/失敗信息。當操作成功完成時,Promise 對象的 then 方法就會被調用;否則就會觸發 catch。如:
const?myFirstPromise?=?new?Promise((resolve,?reject)?=>?{
????setTimeout(function(){
????????resolve("成功!");?
????},?250);
});
myFirstPromise.then((data)?=>?{
????console.log("Yay!?"?+?data);
}).catch((e)?=>?{...});
由于篇幅過長,我將此系列分成上中下三篇,下篇我們在見。
原文:
https://dev.to/macmacky/70-javascript-interview-questions-5gfi#1-whats-the-difference-between-undefined-and-null
. JavaScript 有哪些數據類型,它們的區別?
JavaScript 共有八種數據類型,分別是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是 ES6 中新增的數據類型:
●Symbol 代表創建后獨一無二且不可變的數據類型,它主要是為了 解決可能出現的全局變量沖突的問題。
●BigInt 是一種數字類型的數據,它可以表示任意精度格式的整數,使用 BigInt 可以安全地存儲和操作大整數,即使這個數已經超出了 Number 能夠表示的安全整數范圍。
這些數據可以分為原始數據類型和引用數據類型:
●棧:原始數據類型(Undefined、Null、Boolean、Number、String)●堆:引用數據類型(對象、數組和函數)
兩種類型的區別在于存儲位置的不同:
●原始數據類型直接存儲在棧(stack)中的簡單數據段,占據空間 小、大小固定,屬于被頻繁使用數據,所以放入棧中存儲;
●引用數據類型存儲在堆(heap)中的對象,占據空間大、大小不固 定。如果存儲在棧中,將會影響程序運行的性能;引用數據類型在棧 中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引 用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。
堆和棧的概念存在于數據結構和操作系統內存中,在數據結構中:●在數據結構中,棧中數據的存取方式為先進后出。
●堆是一個優先隊列,是按優先級來進行排序的,優先級可以按照大 小來規定。
在操作系統中,內存被分為棧區和堆區:
●棧區內存由編譯器自動分配釋放,存放函數的參數值,局部變量的 值等。其操作方式類似于數據結構中的棧。
●堆區內存一般由開發者分配釋放,若開發者不釋放,程序結束時可 能由垃圾回收機制回收。
2. 數據類型檢測的方式有哪些
(1)typeof
其中數組、對象、null 都會被判斷為 object,其他判斷都正確。
(2)instanceof
instanceof 可以正確判斷對象的類型,其內部運行機制是判斷在其 原型鏈中能否找到該類型的原型。
可以看到,instanceof 只能正確判斷引用數據類型,而不能判斷其 本數據類型。instanceof 運算符可以用來測試一個對象在其原型鏈 中是否存在一個構造函數的 prototype 屬性。
(3) constructor
constructor 有兩個作用,一是判斷數據的類型,二是對象實例通過 constrcutor 對象訪問它的構造函數。需要注意,如果創建一個對象 來改變它的原型,constructor 就不能用來判斷數據類型了:
(4)Object.prototype.toString.call()
Object.prototype.toString.call() 使用 Object 對象的原型方法 toString 來判斷數據類型:
同樣是檢測對象 obj 調用 toString 方法,obj.toString()的結果和 Object.prototype.toString.call(obj)的結果不一樣,這是為什 么?
這是因為 toString 是 Object 的原型方法,而 Array、function 等類 型作為 Object 的實例,都重寫了 toString 方法。不同的對象類型調 用 toString 方法時,根據原型鏈的知識,調用的是對應的重寫之后
的 toString 方法(function 類型返回內容為函數體的字符串,Array 類型返回元素組成的字符串…),而不會去調用 Object 上原型 toString 方法(返回對象的具體類型),所以采用 obj.toString() 不能得到其對象類型,只能將 obj 轉換為字符串類型;因此,在想要 得到對象的具體類型時,應該調用 Object 原型上的 toString 方法。
3. null 和 undefined 區別
首先 Undefined 和 Null 都是基本數據類型,這兩個基本數據類型 分別都只有一個值,就是 undefined 和 null。
undefined 代表的含義是未定義,null 代表的含義是空對象。一般 變量聲明了但還沒有定義的時候會返回 undefined,null 主要用于 賦值給一些可能會返回對象的變量,作為初始化。
undefined 在 JavaScript 中不是一個保留字,這意味著可以使用 undefined 來作為一個變量名,但是這樣的做法是非常危險的,它會 影響對 undefined 值得判斷。我們可以通過一些方法獲得安全的 undefined 值,比如說 void 0。
當對這兩種類型使用 typeof 進行判斷時,Null 類型化會返回“object”,這是一個歷史遺留的問題。當使用雙等號對兩種類型的 值進行比較時會返回 true,使用三個等號時會返回 false。
4. intanceof 操作符的實現原理及實現
instanceof 運算符用于判斷構造函數的 prototype 屬性是否出現 在對象的原型鏈中的任何位置。
5. 如何獲取安全的 undefined 值?
因為 undefined 是一個標識符,所以可以被當作變量來使用和賦值,但是這樣會影響 undefined 的正常判斷。表達式 void ___ 沒有返 回值,因此返回結果是 undefined。void 并不改變表達式的結果,只是讓表達式不返回值。因此可以用 void 0 來獲得 undefined。
6. Object.is() 與比較操作符 “===”、“==” 的區別?
使用雙等號(==)進行相等判斷時,如果兩邊的類型不一致,則會進 行強制類型轉化后再進行比較。
使用三等號(===)進行相等判斷時,如果兩邊的類型不一致時,不 會做強制類型準換,直接返回 false。
使用 Object.is 來進行相等判斷時,一般情況下和三等號的判斷相 同,它處理了一些特殊的情況,比如 -0 和 +0 不再相等,兩個 NaN 是相等的。
7. 什么是 JavaScript 中的包裝類型?
在 JavaScript 中,基本類型是沒有屬性和方法的,但是為了便于操 作基本類型的值,在調用基本類型的屬性或方法時 JavaScript 會在 后臺隱式地將基本類型的值轉換為對象,如:
在 訪 問 'abc'.length 時 , JavaScript 將 'abc' 在 后 臺 轉 換 成 String('abc'),然后再訪問其 length 屬性。
JavaScript 也可以使用 Object 函數顯式地將基本類型轉換為包裝類 型:
也可以使用 valueOf 方法將包裝類型倒轉成基本類型:
看看如下代碼會打印出什么:
答案是什么都不會打印,因為雖然包裹的基本類型是 false,但是 false 被包裹成包裝類型后就成了對象,所以其非值為 false,所以 循環體中的內容不會運行。
8. 為什么會有 BigInt 的提案?
JavaScript 中 Number.MAX_SAFE_INTEGER 表示最?安全數字,計算 結果是 9007199254740991,即在這個數范圍內不會出現精度丟失(?數除外)。但是?旦超過這個范圍,js 就會出現計算不準確的情況,這在?數計算的時候不得不依靠?些第三?庫進?解決,因此官?提 出了 BigInt 來解決此問題。
9. 如何判斷一個對象是空對象
使用 JSON 自帶的.stringify 方法來判斷:
使用 ES6 新增的方法 Object.keys()來判斷:
10. const 對象的屬性可以修改嗎
const 保證的并不是變量的值不能改動,而是變量指向的那個內存地 址不能改動。對于基本類型的數據(數值、字符串、布爾值),其值 就保存在變量指向的那個內存地址,因此等同于常量。
但對于引用類型的數據(主要是對象和數組)來說,變量指向數據的 內存地址,保存的只是一個指針,const 只能保證這個指針是固定不 變的,至于它指向的數據結構是不是可變的,就完全不能控制了。
11. 如果 new 一個箭頭函數的會怎么樣
箭頭函數是 ES6 中的提出來的,它沒有 prototype,也沒有自己的 this 指向,更不可以使用 arguments 參數,所以不能 New 一個箭頭函數。
new 操作符的實現步驟如下:
1.創建一個對象
2.將構造函數的作用域賦給新對象(也就是將對象的__proto__屬性 指向構造函數的 prototype 屬性)
3.指向構造函數中的代碼,構造函數中的 this 指向該對象(也就是 為這個對象添加屬性和方法)
4.返回新的對象
所以,上面的第二、三步,箭頭函數都是沒有辦法執行的。
12. 箭頭函數的 this 指向哪??
箭頭函數不同于傳統 JavaScript 中的函數,箭頭函數并沒有屬于??的 this,它所謂的 this 是捕獲其所在上下?的 this 值,作為??的 this 值,并且由于沒有屬于??的 this,所以是不會被 new 調?的,這個所謂的 this 也不會被改變。
可以?Babel 理解?下箭頭函數:
轉化后:
13. 擴展運算符的作用及使用場景
(1)對象擴展運算符
對象的擴展運算符(...)用于取出參數對象中的所有可遍歷屬性,拷 貝到當前對象之中。
上述方法實際上等價于:
Object.assign 方法用于對象的合并,將源對象(source)的所有可 枚舉屬性,復制到目標對象(target)。Object.assign 方法的第一 個參數是目標對象,后面的參數都是源對象。(如果目標對象與源對 象有同名屬性,或多個源對象有同名屬性,則后面的屬性會覆蓋前面 的屬性)。
同樣,如果用戶自定義的屬性,放在擴展運算符后面,則擴展運算符 內部的同名屬性會被覆蓋掉。
利用上述特性就可以很方便的修改對象的部分屬性。在 redux 中的 reducer 函數規定必須是一個純函數,reducer 中的 state 對象要求 不能直接修改,可以通過擴展運算符把修改路徑的對象都復制一遍,然后產生一個新的對象返回。
需要注意:擴展運算符對對象實例的拷貝屬于淺拷貝。
(2)數組擴展運算符
數組的擴展運算符可以將一個數組轉為用逗號分隔的參數序列,且每 次只能展開一層數組。
下面是數組的擴展運算符的應用:
將數組轉換為參數序列
復制數組
要記住:擴展運算符(…)用于取出參數對象中的所有可遍歷屬性,拷 貝到當前對象之中,這里參數對象是個數組,數組里面的所有對象都 是基礎數據類型,將所有基礎數據類型重新拷貝到新的數組中。
合并數組
如果想在數組內合并數組,可以這樣:
擴展運算符與解構賦值結合起來,用于生成數組
需要注意:如果將擴展運算符用于數組賦值,只能放在參數的最后一 位,否則會報錯。
將字符串轉為真正的數組
任何 Iterator 接口的對象,都可以用擴展運算符轉為真正的數組
比較常見的應用是可以將某些數據結構轉為數組:
用于替換 es5 中的 Array.prototype.slice.call(arguments)寫法。使用 Math 函數獲取數組中特定的值
14. Proxy 可以實現什么功能?
在 Vue3.0 中通過 Proxy 來替換原本的 Object.defineProperty 來實現數據響應式。
Proxy 是 ES6 中新增的功能,它可以用來自定義對象中的操作。
代表需要添加代理的對象,handler 用來自定義對象中的操作,比如 可以用來自定義 set 或者 get 函數。
下面來通過 Proxy 來實現一個數據響應式:
在上述代碼中,通過自定義 set 和 get 函數的方式,在原本的邏輯 中插入了我們的函數邏輯,實現了在對對象任何屬性進行讀寫時發出 通知。
當然這是簡單版的響應式實現,如果需要實現一個 Vue 中的響應式,需要在 get 中收集依賴,在 set 派發更新,之所以 Vue3.0 要使用 Proxy 替換原本的 API 原因在于 Proxy 無需一層層遞歸為每個屬 性添加代理,一次即可完成以上操作,性能上更好,并且原本的實現 有一些數據更新不能監聽到,但是 Proxy 可以完美監聽到任何方式 的數據改變,唯一缺陷就是瀏覽器的兼容性不好。
15. 常用的正則表達式有哪些?
16. 對 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 數據結構,以此來進行數據的訪 問。
17. JavaScript 腳本延遲加載的方式有哪些?
延遲加載就是等頁面加載完成之后再加載 JavaScript 文件。 js 延 遲加載有助于提高頁面加載速度。
一般有以下幾種方式:
defer 屬性:給 js 腳本添加 defer 屬性,這個屬性會讓腳本的加 載與文檔的解析同步解析,然后在文檔解析完成后再執行這個腳本文 件,這樣的話就能使頁面的渲染不被阻塞。多個設置了 defer 屬性
的腳本按規范來說最后是順序執行的,但是在一些瀏覽器中可能不是 這樣。
async 屬性:給 js 腳本添加 async 屬性,這個屬性會使腳本異步 加載,不會阻塞頁面的解析過程,但是當腳本加載完成后立即執行 js 腳本,這個時候如果文檔沒有解析完成的話同樣會阻塞。多個 async 屬性的腳本的執行順序是不可預測的,一般不會按照代碼的順序依次 執行。
動態創建 DOM 方式:動態創建 DOM 標簽的方式,可以對文檔的加載 事件進行監聽,當文檔加載完成后再動態的創建 script 標簽來引入 js 腳本。
使用 setTimeout 延遲方法:設置一個定時器來延遲加載 js 腳本文 件
讓 JS 最后加載:將 js 腳本放在文檔的底部,來使 js 腳本盡可能 的在最后來加載執行。
18. 什么是 DOM 和 BOM?
DOM 指的是文檔對象模型,它指的是把文檔當做一個對象,這個對象 主要定義了處理網頁內容的方法和接口。
BOM 指的是瀏覽器對象模型,它指的是把瀏覽器當做一個對象來對待,這個對象主要定義了與瀏覽器進行交互的法和接口。BOM 的核心是 window,而 window 對象具有雙重角色,它既是通過 js 訪問瀏覽器 窗口的一個接口,又是一個 Global(全局)對象。這意味著在網頁 中定義的任何對象,變量和函數,都作為全局對象的一個屬性或者方 法存在。window 對象含有 location 對象、navigator 對象、screen
對象等子對象,并且 DOM 的最根本的對象 document 對象也是 BOM 的 window 對象的子對象。
19. escape、encodeURI、encodeURIComponent 的區別
encodeURI 是對整個 URI 進行轉義,將 URI 中的非法字符轉換為合 法字符,所以對于一些在 URI 中有特殊意義的字符不會進行轉義。
encodeURIComponent 是對 URI 的組成部分進行轉義,所以一些特殊 字符也會得到轉義。
escape 和 encodeURI 的作用相同,不過它們對于 unicode 編碼為 0xff 之外字符的時候會有區別,escape 是直接在字符的 unicode 編碼前加上 %u,而 encodeURI 首先會將字符轉換為 UTF-8 的格式,再在每個字節前加上 %。
20. 對 AJAX 的理解,實現一個 AJAX 請求
AJAX 是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的 異步通信,從服務器獲取 XML 文檔從中提取數據,再更新當前網頁的對應部分,而不用刷新整個網頁。
創建 AJAX 請求的步驟:
創建一個 XMLHttpRequest 對象。
在這個對象上使用 open 方法創建一個 HTTP 請求,open 方法所需 要的參數是請求的方法、請求的地址、是否異步和用戶的認證信息。
在發起請求前,可以為這個對象添加一些信息和監聽函數。比如說可 以通過 setRequestHeader 方法來為請求添加頭信息。還可以為這個 對象添加一個狀態監聽函數。一個 XMLHttpRequest 對象一共有 5 個狀態,當它的狀態變化時會觸發 onreadystatechange 事件,可以
通過設置監聽函數,來處理請求成功后的結果。當對象的 readyState 變為 4 的時候,代表服務器返回的數據接收完成,這個時候可以通 過判斷請求的狀態,如果狀態是 2xx 或者 304 的話則代表返回正常。
這個時候就可以通過 response 中的數據來對頁面進行更新了。
當對象的屬性和監聽函數設置完成后,最后調用 sent 方法來向服務 器發起請求,可以傳入參數作為發送的數據體。
使用 Promise 封裝 AJAX:
21. 什么是尾調用,使用尾調用有什么好處?
尾調用指的是函數的最后一步調用另一個函數。代碼執行是基于執行 棧的,所以當在一個函數里調用另一個函數時,會保留當前的執行上 下文,然后再新建另外一個執行上下文加入棧中。使用尾調用的話,因為已經是函數的最后一步,所以這時可以不必再保留當前的執行上 下文,從而節省了內存,這就是尾調用優化。但是 ES6 的尾調用優 化只在嚴格模式下開啟,正常模式是無效的。
22. ES6 模塊與 CommonJS 模塊有什么異同?
ES6 Module 和 CommonJS 模塊的區別:
CommonJS 是對模塊的淺拷?,ES6 Module 是對模塊的引?,即 ES6 Module 只存只讀,不能改變其值,也就是指針指向不能變,類似 const;
import 的接?是 read-only(只讀狀態),不能修改其變量值。 即 不能修改其變量的指針指向,但可以改變變量內部指針指向,可以對 commonJS 對重新賦值(改變指針指向),但是對 ES6 Module 賦值會 編譯報錯。
ES6 Module 和 CommonJS 模塊的共同點:
CommonJS 和 ES6 Module 都可以對引?的對象進?賦值,即對對象內 部屬性的值進?改變。
23. 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 對象。
24. 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 攻擊
25. 對原型、原型鏈的理解
在 JavaScript 中是使用構造函數來新建一個對象的,每一個構造函 數的內部都有一個 prototype 屬性,它的屬性值是一個對象,這個 對象包含了可以由該構造函數的所有實例共享的屬性和方法。當使用 構造函數新建一個對象后,在這個對象的內部將包含一個指針,這個 指針指向構造函數的 prototype 屬性對應的值,在 ES5 中這個指針 被稱為對象的原型。一般來說不應該能夠獲取到這個值的,但是現在 瀏覽器中都實現了 __proto__ 屬性來訪問這個屬性,但是最好不要 使用這個屬性,因為它不是規范中規定的。ES5 中新增了一個 Object.getPrototypeOf() 方法,可以通過這個方法來獲取對象的原 型。
當訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那么 它就會去它的原型對象里找這個屬性,這個原型對象又會有自己的原 型,于是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一 般來說都是 Object.prototype 所以這就是新建的對象為什么能夠 使用 toString() 等方法的原因。
特點:JavaScript 對象是通過引用來傳遞的,創建的每個新對象實 體中并沒有一份屬于自己的原型副本。當修改原型時,與之相關的對 象也會繼承這一改變。
26. 原型鏈的終點是什么?如何打印出原型鏈的終點?
由于 Object 是構造函數,原型鏈終點 Object.prototype.__proto__,而 Object.prototype.__proto__=== null // true,所以,原型鏈 的終點是 null。原型鏈上的所有原型都是對象,所有的對象最終都 是由 Object 構造的,而 Object.prototype 的下一級是
Object.prototype.__proto__。
27. 對作用域、作用域鏈的理解
1)全局作用域和函數作用域
(1)全局作用域
最外層函數和最外層函數外面定義的變量擁有全局作用域
所有未定義直接賦值的變量自動聲明為全局作用域
所有 window 對象的屬性擁有全局作用域
全局作用域有很大的弊端,過多的全局作用域變量會污染全局命名空 間,容易引起命名沖突。
(2)函數作用域
函數作用域聲明在函數內部的變零,一般只有固定的代碼片段可以訪 問到
作用域是分層的,內層作用域可以訪問外層作用域,反之不行 2)塊級作用域
使用 ES6 中新增的 let 和 const 指令可以聲明塊級作用域,塊級作用 域可以在函數中創建也可以在一個代碼塊中的創建(由{ }包裹的代 碼片段)
let 和 const 聲明的變量不會有變量提升,也不可以重復聲明
在循環中比較適合綁定塊級作用域,這樣就可以把聲明的計數器變量 限制在循環內部。
作用域鏈:
在當前作用域中查找所需變量,但是該作用域沒有這個變量,那這個 變量就是自由變量。如果在自己作用域找不到該變量就去父級作用域 查找,依次向上級作用域查找,直到訪問到 window 對象就被終止,這一層層的關系就是作用域鏈。
作用域鏈的作用是保證對執行環境有權訪問的所有變量和函數的有 序訪問,通過作用域鏈,可以訪問到外層環境的變量和函數。
作用域鏈的本質上是一個指向變量對象的指針列表。變量對象是一個 包含了執行環境中所有變量和函數的對象。作用域鏈的前端始終都是 當前執行上下文的變量對象。全局執行上下文的變量對象(也就是全 局對象)始終是作用域鏈的最后一個對象。
當查找一個變量時,如果當前執行環境中沒有找到,可以沿著作用域 鏈向后查找。
28. 對 this 對象的理解
this 是執行上下文中的一個屬性,它指向最后一次調用這個方法的 對象。在實際開發中,this 的指向可以通過四種調用模式來判斷。
第一種是函數調用模式,當一個函數不是一個對象的屬性時,直接作 為函數來調用時,this 指向全局對象。
第二種是方法調用模式,如果一個函數作為一個對象的方法來調用時,this 指向這個對象。
第三種是構造器調用模式,如果一個函數用 new 調用時,函數執行 前會新創建一個對象,this 指向這個新創建的對象。
第四種是 apply 、 call 和 bind 調用模式,這三個方法都可以顯 示的指定調用函數的 this 指向。其中 apply 方法接收兩個參數:一個是 this 綁定的對象,一個是參數數組。call 方法接收的參數,第一個是 this 綁定的對象,后面的其余參數是傳入函數執行的參數。
也就是說,在使用 call() 方法時,傳遞給函數的參數必須逐個列舉 出來。bind 方法通過傳入一個對象,返回一個 this 綁定了傳入對 象的新函數。這個函數的 this 指向除了使用 new 時會被改變,其 他情況下都不會改變。
這四種方式,使用構造器調用模式的優先級最高,然后是 apply、call 和 bind 調用模式,然后是方法調用模式,然后是函數調用模式。
29. call() 和 apply() 的區別?
它們的作用一模一樣,區別僅在于傳入參數的形式的不同。
apply 接受兩個參數,第一個參數指定了函數體內 this 對象的指向,第二個參數為一個帶下標的集合,這個集合可以為數組,也可以為類 數組,apply 方法把這個集合中的元素作為參數傳遞給被調用的函數。
call 傳入的參數數量不固定,跟 apply 相同的是,第一個參數也是 代表函數體內的 this 指向,從第二個參數開始往后,每個參數被依 次傳入函數。
30. 異步編程的實現方式?
JavaScript 中的異步機制可以分為以下幾種:
回調函數 的方式,使用回調函數的方式有一個缺點是,多個回調函 數嵌套的時候會造成回調函數地獄,上下兩層的回調函數間的代碼耦 合度太高,不利于代碼的可維護。
Promise 的方式,使用 Promise 的方式可以將嵌套的回調函數作為 鏈式調用。但是使用這種方法,有時會造成多個 then 的鏈式調用,可能會造成代碼的語義不夠明確。
generator 的方式,它可以在函數的執行過程中,將函數的執行權轉 移出去,在函數外部還可以將執行權轉移回來。當遇到異步函數執行 的時候,將函數執行權轉移出去,當異步函數執行完畢時再將執行權 給轉移回來。因此在 generator 內部對于異步操作的方式,可以以 同步的順序來書寫。使用這種方式需要考慮的問題是何時將函數的控 制權轉移回來,因此需要有一個自動執行 generator 的機制,比如 說 co 模塊等方式來實現 generator 的自動執行。
async 函數 的方式,async 函數是 generator 和 promise 實現的 一個自動執行的語法糖,它內部自帶執行器,當函數內部執行到一個 await 語句的時候,如果語句返回一個 promise 對象,那么函數將 會等待 promise 對象的狀態變為 resolve 后再繼續向下執行。因此 可以將異步邏輯,轉化為同步的順序來書寫,并且這個函數可以自動 執行。
31. 對 Promise 的理解
Promise 是異步編程的一種解決方案,它是一個對象,可以獲取異步 操作的消息,他的出現大大改善了異步編程的困境,避免了地獄回調,它比傳統的解決方案回調函數和事件更合理和更強大。
所謂 Promise,簡單說就是一個容器,里面保存著某個未來才會結束 的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一 個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。
(1)Promise 的實例有三個狀態:
Pending(進行中)
Resolved(已完成)
Rejected(已拒絕)
當把一件事情交給 promise 時,它的狀態就是 Pending,任務完成了 狀態就變成了 Resolved、沒有完成失敗了就變成了 Rejected。
(2)Promise 的實例有兩個過程:
pending -> fulfilled : Resolved(已完成)
pending -> rejected:Rejected(已拒絕)
注意:一旦從進行狀態變成為其他狀態就永遠不能更改狀態了。
Promise 的特點:
對象的狀態不受外界影響。promise 對象代表一個異步操作,有三種 狀態,pending(進行中)、fulfilled(已成功)、rejected(已失 敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他 操作都無法改變這個狀態,這也是 promise 這個名字的由來——“承 諾”;
一旦狀態改變就不會再變,任何時候都可以得到這個結果。promise 對象的狀態改變,只有兩種可能:從 pending 變為 fulfilled,從 pending 變為 rejected。這時就稱為 resolved(已定型)。如果改 變已經發生了,你再對 promise 對象添加回調函數,也會立即得到這 個結果。這與事件(event)完全不同,事件的特點是:如果你錯過 了它,再去監聽是得不到結果的。
Promise 的缺點:
無法取消 Promise,一旦新建它就會立即執行,無法中途取消。
如果不設置回調函數,Promise 內部拋出的錯誤,不會反應到外部。
當處于 pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始 還是即將完成)。
總結:
Promise 對象是異步編程的一種解決方案,最早由社區提出。Promise 是一個構造函數,接收一個函數作為參數,返回一個 Promise 實例。一個 Promise 實例有三種狀態,分別是 pending、resolved 和 rejected,分別代表了進行中、已成功和已失敗。實例的狀態只能由 pending 轉變 resolved 或者 rejected 狀態,并且狀態一經改變,就凝固了,無法再被改變了。
狀態的改變是通過 resolve() 和 reject() 函數來實現的,可以在 異步操作結束后調用這兩個函數改變 Promise 實例的狀態,它的原 型上定義了一個 then 方法,使用這個 then 方法可以為兩個狀態的 改變注冊回調函數。這個回調函數屬于微任務,會在本輪事件循環的 末尾執行。
注意:在構造 Promise 的時候,構造函數內部的代碼是立即執行的
32. Promise 解決了什么問題
在工作中經常會碰到這樣一個需求,比如我使用 ajax 發一個 A 請求 后,成功后拿到數據,需要把數據傳給 B 請求;那么需要如下編寫代 碼:
上面的代碼有如下缺點:
后一個請求需要依賴于前一個請求成功后,將數據往下傳遞,會導致 多個 ajax 請求嵌套的情況,代碼不夠直觀。
如果前后兩個請求不需要傳遞參數的情況下,那么后一個請求也需要 前一個請求成功后再執行下一步操作,這種情況下,那么也需要如上 編寫代碼,導致代碼不夠直觀。
Promise 出現之后,代碼變成這樣:
這樣代碼看起了就簡潔了很多,解決了地獄回調的問題。
33. 對 async/await 的理解
async/await 其實是 Generator 的語法糖,它能實現的效果都能用 then 鏈來實現,它是為優化 then 鏈而開發出來的。從字面上來看,async 是“異步”的簡寫,await 則為等待,所以很好理解 async 用 于申明一個 function 是異步的,而 await 用于等待一個異步方法 執行完成。當然語法上強制規定 await 只能出現在 asnyc 函數中,先 來看看 async 函數返回了什么:
所以,async 函數返回的是一個 Promise 對象。async 函數(包含 函數語句、函數表達式、Lambda 表達式)會返回一個 Promise 對象,如果在函數中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 對象。
async 函數返回的是一個 Promise 對象,所以在最外層不能用 await 獲取其返回值的情況下,當然應該用原來的方式:then() 鏈 來處理這個 Promise 對象,就像這樣:
那如果 async 函數沒有返回值,又該如何?很容易想到,它會返回 Promise.resolve(undefined)。
聯想一下 Promise 的特點——無等待,所以在沒有 await 的情況下 執行 async 函數,它會立即執行,返回一個 Promise 對象,并且,絕不會阻塞后面的語句。這和普通返回 Promise 對象的函數并無二 致。
注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的簡寫,可以用于快速封裝字面量對象或其他對象,將 其封裝成 Promise 實例。
34. async/await 的優勢
單一的 Promise 鏈并不能發現 async/await 的優勢,但是,如果需 要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來 了(很有意思,Promise 通過 then 鏈來解決多層回調的問題,現在 又用 async/await 來進一步優化它)。
假設一個業務,分多個步驟完成,每個步驟都是異步的,而且依賴于 上一個步驟的結果。仍然用 setTimeout 來模擬異步操作:
現在用 Promise 方式來實現這三個步驟的處理:
輸出結果 result 是 step3() 的參數 700 + 200 = 900。doIt() 順 序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一致。
如果用 async/await 來實現呢,會是這樣:
結果和之前的 Promise 實現是一樣的,但是這個代碼看起來是不是
清晰得多,幾乎跟同步代碼一樣
35. async/await 對比 Promise 的優勢
代碼讀起來更加同步,Promise 雖然擺脫了回調地獄,但是 then 的
鏈式調?也會帶來額外的閱讀負擔
Promise 傳遞中間值?常麻煩,?async/await?乎是同步的寫法,?常優雅
錯誤處理友好,async/await 可以?成熟的 try/catch,Promise 的 錯誤捕獲?常冗余
調試友好,Promise 的調試很差,由于沒有代碼塊,你不能在?個返 回表達式的箭頭函數中設置斷點,如果你在?個.then 代碼塊中使?調試器的步進(step-over)功能,調試器并不會進?后續的.then 代 碼塊,因為調試器只能跟蹤同步代碼的每?步。
36. 對象創建的方式有哪些?
一般使用字面量的形式直接創建對象,但是這種創建方式對于創建大
量相似對象的時候,會產生大量的重復代碼。但 js 和一般的面向對
象的語言不同,在 ES6 之前它沒有類的概念。但是可以使用函數來
進行模擬,從而產生出可復用的對象創建方式,常見的有以下幾種:
(1)第一種是工廠模式,工廠模式的主要工作原理是用函數來封裝 創建對象的細節,從而通過調用函數來達到復用的目的。但是它有一 個很大的問題就是創建出來的對象無法和某個類型聯系起來,它只是 簡單的封裝了復用代碼,而沒有建立起對象和類型間的關系。
(2)第二種是構造函數模式。js 中每一個函數都可以作為構造函數,只要一個函數是通過 new 來調用的,那么就可以把它稱為構造函數。執行構造函數首先會創建一個對象,然后將對象的原型指向構造函數 的 prototype 屬性,然后將執行上下文中的 this 指向這個對象,最后再執行整個函數,如果返回值不是對象,則返回新建的對象。因 為 this 的值指向了新建的對象,因此可以使用 this 給對象賦值。構造函數模式相對于工廠模式的優點是,所創建的對象和構造函數建 立起了聯系,因此可以通過原型來識別對象的類型。但是構造函數存 在一個缺點就是,造成了不必要的函數對象的創建,因為在 js 中函 數也是一個對象,因此如果對象屬性中如果包含函數的話,那么每次 都會新建一個函數對象,浪費了不必要的內存空間,因為函數是所有 的實例都可以通用的。
(3)第三種模式是原型模式,因為每一個函數都有一個 prototype 屬性,這個屬性是一個對象,它包含了通過構造函數創建的所有實例 都能共享的屬性和方法。因此可以使用原型對象來添加公用屬性和方 法,從而實現代碼的復用。這種方式相對于構造函數模式來說,解決 了函數對象的復用問題。但是這種模式也存在一些問題,一個是沒有 辦法通過傳入參數來初始化值,另一個是如果存在一個引用類型如 Array 這樣的值,那么所有的實例將共享一個對象,一個實例對引用 類型值的改變會影響所有的實例。
(4)第四種模式是組合使用構造函數模式和原型模式,這是創建自 定義類型的最常見方式。因為構造函數模式和原型模式分開使用都存 在一些問題,因此可以組合使用這兩種模式,通過構造函數來初始化 對象的屬性,通過原型對象來實現函數方法的復用。這種方法很好的 解決了兩種模式單獨使用時的缺點,但是有一點不足的就是,因為使 用了兩種不同的模式,所以對于代碼的封裝性不夠好。
(5)第五種模式是動態原型模式,這一種模式將原型方法賦值的創 建過程移動到了構造函數的內部,通過對屬性是否存在的判斷,可以 實現僅在第一次調用函數時對原型對象賦值一次的效果。這一種方式 很好地對上面的混合模式進行了封裝。
(6)第六種模式是寄生構造函數模式,這一種模式和工廠模式的實 現基本相同,我對這個模式的理解是,它主要是基于一個已有的類型,在實例化時對實例化的對象進行擴展。這樣既不用修改原來的構造函 數,也達到了擴展對象的目的。它的一個缺點和工廠模式一樣,無法 實現對象的識別。
37. 對象繼承的方式有哪些?
(1)第一種是以原型鏈的方式來實現繼承,但是這種實現方式存在 的缺點是,在包含有引用類型的數據時,會被所有的實例對象所共享,容易造成修改的混亂。還有就是在創建子類型的時候不能向超類型傳 遞參數。
(2)第二種方式是使用借用構造函數的方式,這種方式是通過在子 類型的函數中調用超類型的構造函數來實現的,這一種方法解決了不 能向超類型傳遞參數的缺點,但是它存在的一個問題就是無法實現函 數方法的復用,并且超類型原型定義的方法子類型也沒有辦法訪問到。
(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用構造函數 組合起來使用的一種方式。通過借用構造函數的方式來實現類型的屬 性的繼承,通過將子類型的原型設置為超類型的實例來實現方法的繼 承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由于我 們是以超類型的實例來作為子類型的原型,所以調用了兩次超類的構 造函數,造成了子類型的原型中多了很多不必要的屬性。
(4)第四種方式是原型式繼承,原型式繼承的主要思路就是基于已 有的對象來創建新的對象,實現的原理是,向函數中傳入一個對象,然后返回一個以這個對象為原型的對象。這種繼承的思路主要不是為 了實現創造一種新的類型,只是對某個對象實現一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現。缺點與原 型鏈方式相同。
(5)第五種方式是寄生式繼承,寄生式繼承的思路是創建一個用于 封裝繼承過程的函數,通過傳入一個對象,然后復制一個對象的副本,然后對象進行擴展,最后返回這個對象。這個擴展的過程就可以理解 是一種繼承。這種繼承的優點就是對一個簡單對象實現繼承,如果這 個對象不是自定義類型時。缺點是沒有辦法實現函數的復用。
(6)第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超類 型的實例做為子類型的原型,導致添加了不必要的原型屬性。寄生式 組合繼承的方式是使用超類型的原型的副本來作為子類型的原型,這 樣就避免了創建不必要的屬性。
38. 哪些情況會導致內存泄漏
以下四種情況會造成內存的泄漏:
意外的全局變量:由于使用未聲明的變量,而意外的創建了一個全局 變量,而使這個變量一直留在內存中無法被回收。
被遺忘的計時器或回調函數:設置了 setInterval 定時器,而忘記 取消它,如果循環函數有對外部變量的引用的話,那么這個變量會被 一直留在內存中,而無法被回收。
脫離 DOM 的引用:獲取一個 DOM 元素的引用,而后面這個元素被刪 除,由于一直保留了對這個元素的引用,所以它也無法被回收。
閉包:不合理的使用閉包,從而導致某些變量一直被留在內存當中。
ES6(ECMAScript 2015)及之后的版本引入了許多讓JavaScript編程更加高效、簡潔的新特性。以下是其中一些關鍵特性及示例:
const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map(n => n * n); // 使用箭頭函數
const name = "World";
console.log(`Hello, ${name}!`); // 使用模板字符串
Promise是解決JavaScript異步編程的一個重要概念。它代表了一個尚未完成,但未來會完成的操作。Promise有三種狀態:pending(等待中)、fulfilled(已成功)和rejected(已失敗)。
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Data fetched"), 2000);
});
};
fetchData().then(data => console.log(data)); // 輸出:Data fetched
async和await是基于Promise的語法糖,使異步代碼看起來和同步代碼類似,從而簡化了異步操作的書寫。
const fetchData = async () => {
const data = await new Promise((resolve, reject) => {
setTimeout(() => resolve("Data fetched with async/await"), 2000);
});
console.log(data);
};
fetchData(); // 輸出:Data fetched with async/await
現代JavaScript框架,如Vue.js,大量使用了ES6+的新特性來提升開發效率和程序的可讀性。例如,Vue 3 利用了Proxy來實現響應式系統,同時也在其組件系統中廣泛使用了ES6+的特性,如模塊導入、箭頭函數等。
函數式編程是一種編程范式,它將計算視為數學函數的評估,并避免改變狀態和可變數據。JavaScript作為一種多范式語言,支持函數式編程的核心概念,如純函數、高階函數等。
const pureAdd = (a, b) => a + b; // 純函數
const withLogging = fn => (...args) => {
console.log(`Calling function with arguments: ${args}`);
const result = fn(...args);
console.log(`Function returned: ${result}`);
return result;
};
const add = (a, b) => a + b;
const addWithLogging = withLogging(add);
addWithLogging(2, 3); // 日志輸出調用細節和結果
在現代Web開發中,構建高效的動態用戶界面(UI)是至關重要的。JavaScript通過操作DOM(文檔對象模型)使得開發者能夠創建富交互的Web應用。通過監聽用戶的行為(如點擊、滾動等事件),JavaScript可以動態地修改HTML和CSS,提供動態內容的加載、動畫效果以及即時的用戶反饋。
document.getElementById('addButton').addEventListener('click', function() {
const listItem = document.createElement('li');
listItem.textContent = '新的列表項';
document.getElementById('myList').appendChild(listItem);
});
隨著單頁應用(SPA)的流行,狀態管理變得越來越復雜。狀態管理是指在用戶與應用交互過程中,對狀態的有效管理和更新。常見的狀態管理方案包括Redux、Vuex(Vue的狀態管理方案)、以及React的Context API。
Redux示例:
Redux通過一個全局的store來存儲狀態,組件通過dispatch發送action來更新狀態,通過subscribe監聽狀態變化。
// 創建Reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// 創建store
let store = Redux.createStore(counter);
store.subscribe(() => console.log(store.getState()));
// 發送action
store.dispatch({ type: 'INCREMENT' });
模塊化是指將大型的JavaScript應用分解成小的、可維護的、可重用的代碼塊。ES6引入了import和export語法,使得模塊化開發成為可能。
示例:
// math.js
export function add(x, y) {
return x + y;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3));
Web性能優化是提高用戶體驗的關鍵。它包括減少頁面加載時間、優化渲染路徑、減少JavaScript執行時間等。一些常見的性能優化技巧包括使用壓縮和合并資源文件、懶加載圖片和腳本、利用瀏覽器緩存等。
懶加載圖片示例:
使用Intersection Observer API實現圖片的懶加載。
document.addEventListener("DOMContentLoaded", function() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
});
JavaScript的工程化是指使用工具和流程來提高開發效率和代碼質量。自動化測試則確保代碼更改不會破壞現有功能。
構建工具示例:Webpack
Webpack是一個模塊打包器,它可以處理依賴并將模塊打包成靜態資源。
自動化測試示例:Jest
Jest是一個JavaScript測試框架,支持單元測試和集成測試。
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
在Web開發中,安全是一個重要考慮因素。常見的Web安全問題包括跨站腳本攻擊(XSS)、跨站請求偽造(CSRF)、SQL注入等。
// 使用DOMPurify清理HTML
const clean = DOMPurify.sanitize(dirtyHTML);
document.getElementById('someDiv').innerHTML = clean;
響應式編程是一種異步編程范式,關注于數據流和變化傳播。RxJS是一個庫,用于使用可觀察對象來編寫響應式編程代碼,使得處理事件、異步請求和回調更加簡潔。
示例:從輸入框中捕獲輸入事件
const { fromEvent } = rxjs;
const input = document.querySelector('input');
const observable = fromEvent(input, 'input');
observable.subscribe(event => console.log(event.target.value));
在JavaScript項目中的選擇:如果項目需要高度靈活的數據查詢能力,GraphQL可能是更好的選擇。如果項目有簡單的數據需求,或者已經存在RESTful API的基礎設施,那么繼續使用REST可能更合適。
TypeScript是JavaScript的一個超集,添加了類型系統和對ES6+的支持。TypeScript提供的類型安全可以幫助開發者在編譯時期捕獲錯誤,提高代碼的質量和可維護性。
示例:一個簡單的TypeScript函數
function greet(person: string, date: Date): string {
return `Hello ${person}, today is ${date.toDateString()}!`;
}
console.log(greet("Maddy", new Date()));
漸進式Web應用(PWA)是一種可以提供類似于原生應用體驗的Web應用。通過Service Workers,PWA可以在離線時仍然提供核心功能。
注冊一個Service Worker示例:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function(err) {
console.log('Service Worker registration failed:', err);
});
}
在sw.js中,你可以緩存關鍵資源,攔截網絡請求,并提供離線支持。
WebAssembly (Wasm) 是一種新的編碼方式,讓在網頁中運行的代碼可以接近原生執行速度,通過提供一個新的、高效的格式來在網頁上運行代碼。它對前端開發的主要影響是性能的顯著提升,特別是對于需要大量計算的應用,如游戲、視頻編輯、圖像處理等。
使用場景:
示例:將C/C++代碼編譯為WebAssembly并在網頁中運行。
<script>
fetch('example.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(results => {
results.instance.exports.exportedFunc();
});
</script>
D3.js 是一個JavaScript庫,用于使用Web標準制作動態、交互式的數據可視化。它強大之處在于能夠綁定任意數據到DOM元素,并且對數據進行動態變化的表示,如通過SVG、Canvas來繪圖。
示例:使用D3.js創建一個簡單的柱狀圖。
<script src="https://d3js.org/d3.v6.js"></script>
<script>
const data = [4, 8, 15, 16, 23, 42];
const width = 420,
barHeight = 20;
const x = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, width]);
const chart = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", barHeight * data.length);
const bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", (d, i) => "translate(0," + i * barHeight + ")");
bar.append("rect")
.attr("width", x)
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", d => x(d) - 3)
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(d => d);
</script>
在前端安全中,防范XSS和CSRF攻擊是兩個主要的考慮點。
服務端渲染(SSR)是指JavaScript和應用程序界面在服務器上渲染成HTML,然后發送到客戶端的過程。這對SEO非常有利,因為搜索引擎爬蟲可以直接分析渲染好的頁面內容。
優勢:
前端監控是指對Web應用的性能、錯誤、用戶行為等進行實時監控。這有助于及時發現問題,優化用戶體驗和應用性能。
性能指標:
工具:Google的Lighthouse、WebPageTest、Chrome DevTools等都是優秀的性能監控和分析工具。
CSS-in-JS 是一種在JavaScript中編寫CSS的技術,它允許開發者在JS文件中直接包含CSS樣式,從而使樣式和組件邏輯緊密耦合。這種做法有助于避免樣式沖突,使得組件更加可重用。
框架選擇:流行的CSS-in-JS庫包括Styled-components、Emotion 和 JSS。這些庫提供了將CSS寫入JavaScript的能力,同時保持了CSS的動態性和復用性。
代碼示例(使用Styled-components):
import styled from 'styled-components';
const Button = styled.button`
background: palevioletred;
color: white;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
// 在組件中使用
render() {
return <Button>點擊我</Button>;
}
Web組件 是一套不同的技術,允許開發者創建可重用的定制元素 — 并且它們的功能性在Web應用中是封裝的。Web組件的核心技術包括Custom Elements、Shadow DOM和HTML templates。
微前端 是一種設計思想,允許將前端應用分解為更小、獨立交付的片段。每個團隊可以獨立開發和部署自己的功能區塊,最終集成到一個更大的應用中。微前端架構的目的是促進團隊之間的技術棧獨立性,加速開發流程。
使用JavaScript進行跨平臺開發的策略包括React Native和Electron。
這些工具和框架讓開發者能夠重用代碼并保持應用在不同平臺間的一致性。
設計模式是解決常見問題的可重用解決方案。
let instance = null;
class Singleton {
constructor() {
if (!instance) {
instance = this;
}
return instance;
}
}
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
JavaScript的垃圾回收機制是自動的,但是開發者仍需注意避免內存泄漏。內存泄漏通常是由不再需要的對象引用導致的,使得垃圾回收器無法釋放它們。
WebGL是一個JavaScript API,用于在不需要插件的情況下,在任何兼容的Web瀏覽器內渲染高性能的交互式2D和3D圖形。它通過引入與OpenGL ES 2.0緊密一致的API,使得可以在HTML <canvas> 元素中使用,從而充分利用用戶設備提供的硬件圖形加速?。為了開始使用WebGL,你需要創建一個canvas元素并在你的JavaScript代碼中初始化WebGL上下文??。對于想要深入了解和實踐WebGL,探索3D游戲開發,MDN提供了關于如何構建基本示例,包括使用A-Frame、Babylon.js、PlayCanvas和Three.js等流行框架的步驟??。
Node.js的出現使得JavaScript能夠被用于服務器端編程,從而開啟了全棧開發的大門,允許開發者使用同一種語言來編寫前端和后端代碼。這意味著開發者可以構建和維護具有復雜功能的Web應用,同時保持技術棧的一致性。Node.js的非阻塞I/O模型特別適合處理大量并發連接,使其成為構建高性能Web應用的理想選擇。
隨著前端應用的復雜性增加,采用恰當的架構模式變得至關重要。微服務架構允許將應用分解為小的、松耦合的服務,每個服務執行特定的業務功能,并通過輕量級通信機制進行交互。這種方式提高了應用的可維護性和可擴展性。另一種模式是后端為前端(BFF),它介于客戶端和微服務之間,為客戶端提供精確的API,進一步優化了數據交換和應用性能。
為了觸及全球用戶,前端應用需要支持多語言。國際化(i18n)是設計和開發應用以便它可以輕松地適配不同語言和地區,而不需要進行重大改動的過程。本地化(l10n)是將應用適配到特定區域或語言的過程,包括翻譯文本和適配格式(如日期和貨幣)。通過使用國際化庫如i18next,開發者可以更容易地實現多語言支持。
Web3和區塊鏈技術正在改變前端開發的景觀,提供了創建去中心化應用(DApps)的新方法。這些應用運行在區塊鏈上,提供高度的透明度、安全性和不可篡改性。Web3技術使得前端開發者能夠直接與區塊鏈交互,為用戶提供全新的在線交互體驗。隨著這些技術的成熟和發展,預計將會出現更多創新的Web應用和服務。
如果喜歡,可以點贊收藏,關注喲~
*請認真填寫需求信息,我們會在24小時內與您取得聯系。