JavaScript 是一種使用者端程式語言。 全球超過90%的網站都在使用它,它是世界上最常用的程式語言之一。 因此,今天欄目來討論 10 個有關 JavaScript 的常見問題。
思路:首先,使用indexOf
查詢要刪除的陣列元素的索引(index)
,然後使用splice
方法刪除該索引所對應的項。
splice()
是一個非純函數,通過刪除現有元素和/或新增新元素來更改陣列的內容。
const array = [2, 5, 9] const index = array.indexOf(5) if (index > -1) { array.splice(index, 1) } console.log(array) // [ 2, 9 ]複製程式碼
splice
的第二個引數是要刪除的元素數量。注意,splice
會在適當的位置修改陣列,並返回一個包含已刪除元素的新陣列。
接著,我們可以來完善一下。下面有兩個函數,第一個函數僅刪除一個匹配項(即從[2,5,9,1,5,8,5]
中刪除第一個匹配項5
),而第二個函數則刪除所有匹配項:
// 僅刪除第一個匹配項 function removeItemOnce (arr, value) { let index = arr.indexOf(value) if (index > -1) { arr.splice(index, 1) } return arr } // 刪除所有匹配項 function removeItemAll (arr, value) { let i = 0 while(i < arr.length) { if (arr[i] === value) { arr.splice(i, 1) } else { ++i } } }複製程式碼
刪除陣列中索引i
處的元素:
刪除陣列中索引i
處的元素:
array.splice(i, 1)複製程式碼
如果你想從陣列中刪除值為number
的每個元素,可以這樣做:
for (let i = array.length - 1; i>=0; i--) { if (array[i] === number) { array.splice(i, 1) } }複製程式碼
如果你只想使索引i
處的元素不再存在,但又不想更改其他元素的索引:
delete array[i]複製程式碼
jQuery 不是必需的,window.location.replace(…)
最適合模擬 HTTP 重定向。window.location.replace(...)
優於使用window.location.href
,因為replace()
不會將原始頁面保留在對談歷史記錄中,這意味著使用者將不會陷入永無休止回退按鈕。
如果要模擬單擊連結,可以使用location.href
,如果要模擬HTTP重定向,請使用location.replace
。
事例:
//模擬HTTP重定向 window.location.replace("http://stackoverflow.com") // 模擬單擊連結 window.location.href = "http://stackoverflow.com"複製程式碼
你還可以這樣做:
$(location).attr('href', 'http://stackoverflow.com')複製程式碼
閉包是一個函數和對該函數外部作用域的參照(詞法環境),詞法環境是每個執行上下文(堆疊)的一部分,並且是識別符號(即區域性變數名稱)和值之間的對映。
JavaScript 中的每個函數都維護對其外部詞法環境的參照。此參照用於設定呼叫函數時建立的執行上下文。不管何時呼叫函數,該參照使函數內的程式碼能夠檢視在函數外宣告的變數。
在下面的程式碼中,inner
與呼叫foo
時建立的執行上下文的詞法環境一起形成一個閉包,並對外部隱藏了變數secret
:
function foo() { const secret = Math.trunc(Math.random()*100) return function inner() { console.log(`The secret number is ${secret}.`) } } const f = foo() // secret 不能從foo 外部直接存取 f() // 存取 secret 的唯一辦法就是呼叫 f複製程式碼
換句話說,在JavaScript中,函數帶有對私有狀態的參照,只有它們(以及在相同的詞法環境中宣告的任何其他函數)可以存取該私有狀態。這個狀態對函數的呼叫者是不可見的,這為資料隱藏和封裝提供了一種優秀的機制。
請記住,JavaScript中的函數可以像變數一樣傳遞,這意味著這些功能和狀態的對可以在程式中傳遞:類似於在c++中傳遞類的範例。
如果JavaScript沒有閉包,則必須在函數之間顯式傳遞更多狀態,從而使參數列更長,程式碼更冗餘。
所以,如果你想讓一個函數總是能夠存取私有狀態,你可以使用一個閉包,我們經常想把狀態和函數聯絡起來。例如,在Java或c++中,當你向類新增私有範例變數和方法時,這是將狀態與功能關聯起來。
在 C 語言和大多數其他程式語言中,函數返回後,由於堆疊被銷燬,所有的區域性變數都不再可存取。在JavaScript中,如果在另一個函數中宣告一個函數,那麼外部函數的本地變數在返回後仍然可以存取。這樣,在上面的程式碼中,secret
在從foo
返回後仍然對函數物件內部可用。
閉包在需要與函數關聯的私有狀態時非常有用。這是一個非常常見的場景,JavaScript直到2015年才有類語法,它仍然沒有私有欄位語法,閉包滿足了這一需求。
私有範例變數
在下面的事例中,函數 toString
隱藏了 Car 類的一些細節。
function Car(manufacturer, model, year, color) { return { toString() { return `${manufacturer} ${model} (${year}, ${color})` } } } const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver') console.log(car.toString())複製程式碼
函數語言程式設計
在下面的程式碼中,函數inner
隱藏了fn
和args
。
function curry(fn) { const args = [] return function inner(arg) { if(args.length === fn.length) return fn(...args) args.push(arg) return inner } } function add(a, b) { return a + b } const curriedAdd = curry(add) console.log(curriedAdd(2)(3)()) // 5複製程式碼
面向事件的程式設計
在以下程式碼中,函數onClick
隱藏了變數BACKGROUND_COLOR
。
const $ = document.querySelector.bind(document) const BACKGROUND_COLOR = 'rgba(200,200,242,1)' function onClick() { $('body').style.background = BACKGROUND_COLOR } $('button').addEventListener('click', onClick)複製程式碼
<button>Set background color</button>複製程式碼
模組化
在下面的範例中,所有實現細節都隱藏在一個立即執行的函數表示式中。函數tick
和toString
隱藏了私有狀態和函數,它們需要完成自己的工作。閉包使我們能夠模組化和封裝我們的程式碼。
let namespace = {}; (function foo(n) { let numbers = [] function format(n) { return Math.trunc(n) } function tick() { numbers.push(Math.random() * 100) } function toString() { return numbers.map(format) } n.counter = { tick, toString } }(namespace)) const counter = namespace.counter counter.tick() counter.tick() console.log(counter.toString())複製程式碼
事例 1:
此範例演示區域性變數未在閉包中複製。 閉包保留對原始變數本身的參照。 似乎即使外部函數退出後,堆疊仍在記憶體中保留。
function foo () { let x = 42 let inner = function () { console.log(x) } x = x + 1 return inner } let f = foo() f()複製程式碼
事例 2:
在下面的程式碼中,三種方法log
,increment
和update
都在同一詞法環境閉包中。
function createObject() { let x = 42; return { log() { console.log(x) }, increment() { x++ }, update(value) { x = value } } } const o = createObject() o.increment() o.log() // 43 o.update(5) o.log() // 5 const p = createObject() p.log() // 42複製程式碼
事例 3:
如果使用的變數是使用var
宣告的,需要注意的一點是,使用var
宣告的變數被提升。 由於引入了let
和cons
t,這在現代JavaScript 中幾乎沒有問題。
在下面的程式碼中,每次迴圈中,都會建立一個新的inner
函數,變數i
被覆蓋,但是因var
會讓 i
提升到函數的頂部,所以所有這些inner
函數覆蓋的都是同一個變數,這意味著i(3)
的最終值被列印了三次。
function foo () { var result = [] for (var i = 0; i < 3; i++) { result.push(function inner () { console.log(i) }) } return result } const result = foo() for(var i = 0; i < 3; i++) { result[i]() } // 3 3 3複製程式碼
最後一點:
每當在JavaScript中宣告函數時,都會建立一個閉包。
從一個函數內部返回另一個函數是閉包的經典例子,因為外部函數內部的狀態對於返回的內部函數是隱式可用的,即使外部函數已經完成執行。
只要在函數內使用eval()
,就會使用一個閉包。eval
的文字可以參照函數的區域性變數,在非嚴格模式下,甚至可以通過使用eval('var foo = ')
建立新的區域性變數。
new Function()
(Function constructor)時,它不會覆蓋其詞法環境,而是覆蓋全域性上下文。新函數不能參照外部函數的區域性變數。宣告函數時建立一個閉包。 當呼叫函數時,此閉包用於設定執行上下文。
每次呼叫函數時都會建立一組新的區域性變數。
JavaScript 中的每個函數都維護與其外部詞法環境的連結。 詞法環境是所有名稱的對映(例如,變數,引數)及其範圍內的值。因此,只要看到function
關鍵字,函數內部的程式碼就可以存取在函數外部宣告的變數。
function foo(x) { var tmp = 3; function bar(y) { console.log(x + y + (++tmp)); // 16 } bar(10); } foo(2);複製程式碼
上面輸出結果是16
,引數x
和變數tmp
都存在於外部函數foo
的詞法環境中。函數bar
及其與函數foo
的詞法環境的連結是一個閉包。
函數不必返回即可建立閉包。 僅僅憑藉其宣告,每個函數都會在其封閉的詞法環境中關閉,從而形成一個閉包。
function foo(x) { var tmp = 3; return function (y) { console.log(x + y + (++tmp)); // 16 } } var bar = foo(2); bar(10); // 16 bar(10); // 17複製程式碼
上面還是列印16
,因為bar
內的程式碼仍然可以參照引數x
和變數tmp
,即使它們不再直接的作用域內。
但是,由於tmp
仍然在bar
的閉包內部徘徊,因此可以對其進行遞增。 每次呼叫bar時,它將增加1
。
閉包最簡單的例子是這樣的:
var a = 10; function test() { console.log(a); // will output 10 console.log(b); // will output 6 } var b = 6; test();複製程式碼
當呼叫一個JavaScript函數時,將建立一個新的執行上下文ec
。連同函數引數和目標物件,這個執行上下文還接收到呼叫執行上下文的詞法環境的連結,這意味著在外部詞法環境中宣告的變數(在上面的例子中,a
和b
)都可以從ec
獲得。
每個函數都會建立一個閉包,因為每個函數都有與其外部詞法環境的連結。
注意,變數本身在閉包中是可見的,而不是副本。
參照一些有趣的部分:
嚴格模式是ECMAScript 5中的一個新特性,它允許我們將程式或函數放置在嚴格的操作上下文中。這種嚴格的上下文會防止某些操作被執行,並引發更多異常。
嚴格模式在很多方面都有幫助:
另外,請注意,我信可以將「strict mode」
應用於整個檔案,也可以僅將其用於特定函數。
// Non-strict code... (function(){ "use strict"; // Define your library strictly... })(); // Non-strict code... 複製程式碼
如果是在混合使用舊程式碼和新程式碼的情況,這可能會有所幫助。它有點像在Perl中使用的「use strict」。通過檢測更多可能導致損壞的東西,幫助我們減少更多的錯誤。
現在所有主流瀏覽器都支援嚴格模式。
在原生ECMAScript模組(帶有import
和export
語句)和ES6類中,嚴格模式始終是啟用的,不能禁用。
ECMAScript 6 引入了string .prototype.include
const string = "foo"; const substring = "oo"; console.log(string.includes(substring));複製程式碼
不過,IE 不支援 includes
。在 CMAScript 5或更早的環境中,使用String.prototype.indexOf
。如果找不到子字串,則返回-1
:
var string = "foo"; var substring = "oo"; console.log(string.indexOf(substring) !== -1);複製程式碼
為了使其在舊的瀏覽器中執行,可以使用這種polyfill
:
if (!String.prototype.includes) { String.prototype.includes = function(search, start) { 'use strict'; if (typeof start !== 'number') { start = 0; } if (start + search.length > this.length) { return false; } else { return this.indexOf(search, start) !== -1; } }; }複製程式碼
不同之處在於functionOne
是一個函數表示式,因此只在到達這一行時才會定義,而functionTwo
是一個函數宣告,在它周圍的函數或指令碼被執行(由於提升)時就定義。
如,函數表示式
// TypeError: functionOne is not a function functionOne(); var functionOne = function() { console.log("Hello!"); };複製程式碼
函數宣告:
// "Hello!" functionTwo(); function functionTwo() { console.log("Hello!"); }複製程式碼
過去,在不同的瀏覽器之間,在塊中定義的函數宣告的處理是不一致的。嚴格模式(在ES5中引入)解決了這個問題,它將函數宣告的範圍限定在其封閉的塊上。
'use strict'; { // note this block! function functionThree() { console.log("Hello!"); } } functionThree(); // ReferenceError複製程式碼
function abc(){}
也具有作用域-名稱abc
在遇到該定義的作用域中定義。 例:
function xyz(){ function abc(){}; // abc 在這裡定義... } // ...不是在這裡複製程式碼
如果想在所有瀏覽器上給函數起別名,可以這麼做:
function abc(){}; var xyz = abc;複製程式碼
在本例中,xyz和abc都是同一個物件的別名
console.log(xyz === abc) // true複製程式碼
它的名稱是自動分配的。但是當你定義它的時候
var abc = function(){}; console.log(abc.name); // ""複製程式碼
它的name
稱為空,我們建立了一個匿名函數並將其分配給某個變數。使用組合樣式的另一個很好的理由是使用簡短的內部名稱來參照自身,同時為外部使用者提供一個長而不會衝突的名稱:
// 假設 really.long.external.scoped 為 {} really.long.external.scoped.name = function shortcut(n){ // 它遞迴地呼叫自己: shortcut(n - 1); // ... // 讓它自己作為回撥傳遞:: someFunction(shortcut); // ... }複製程式碼
在上面的例子中,我們可以對外部名稱進行同樣的操作,但是這樣做太笨拙了(而且速度更慢)。另一種參照自身的方法是arguments.callee
,這種寫法也相對較長,並且在嚴格模式中不受支援。
實際上,JavaScript對待這兩個語句是不同的。下面是一個函數宣告:
function abc(){}複製程式碼
這裡的abc
可以定義在當前作用域的任何地方:
// 我們可以在這裡呼叫 abc(); // 在這裡定義 function abc(){} // 也可以在這裡呼叫 abc(); 複製程式碼
此外,儘管有 return
語句,也可以提升:
// 我們可以在這裡呼叫 abc(); return; function abc(){}複製程式碼
下面是一個函數表示式:
var xyz = function(){};複製程式碼
這裡的xyz
是從賦值點開始定義的:
// 我們不可以在這裡呼叫 xyz(); // 在這裡定義 xyz xyz = function(){} // 我們可以在這裡呼叫 xyz(); 複製程式碼
函數宣告與函數表示式之間存在差異的真正原因。
var xyz = function abc(){}; console.log(xyz.name); // "abc"複製程式碼
就個人而言,我們更喜歡使用函數表示式宣告,因為這樣可以控制可見性。當我們像這樣定義函數時:
var abc = function(){};複製程式碼
我們知道,如果我們沒有在作用域鏈的任何地方定義abc
,那麼我們是在全域性作用域內定義的。即使在eval()
內部使用,這種型別的定義也具有彈性。而定義:
function abc(){};複製程式碼
取決於上下文,並且可能讓你猜測它的實際定義位置,特別是在eval()
的情況下,—取決於瀏覽器。
我們可以這樣刪除物件的屬性:
delete myObject.regex; // 或者 delete myObject['regex']; // 或者 var prop = "regex"; delete myObject[prop];複製程式碼
事例:
var myObject = { "ircEvent": "PRIVMSG", "method": "newURI", "regex": "^http://.*" }; delete myObject.regex; console.log(myObject);複製程式碼
JavaScript 中的物件可以看作鍵和值之間的對映。delete
操作符用於一次刪除一個鍵(通常稱為物件屬性)。
var obj = { myProperty: 1 } console.log(obj.hasOwnProperty('myProperty')) // true delete obj.myProperty console.log(obj.hasOwnProperty('myProperty')) // false複製程式碼
delete
操作符不是直接釋放記憶體,它不同於簡單地將null
或undefined
值賦給屬性,而是將屬性本身從物件中刪除。
注意,如果已刪除屬性的值是參照型別(物件),而程式的另一部分仍然持有對該物件的參照,那麼該物件當然不會被垃圾收集,直到對它的所有參照都消失。
delete
只對其描述符標記為configurable
的屬性有效。
嚴格相等運運算元(===
)的行為與抽象相等運運算元(==
)相同,除非不進行型別轉換,而且型別必須相同才能被認為是相等的。
==
運運算元會進行型別轉換後比較相等性。 ===
運運算元不會進行轉換,因此如果兩個值的型別不同,則===
只會返回false。
JavaScript有兩組相等運運算元:===
和!==
,以及它們的孿生兄弟==
和!=
。如果這兩個運算元具有相同的型別和相同的值,那麼===
的結果就是 true
,而!==
的結果就是 false
。
下面是一些事例:
'' == '0' // false 0 == '' // true 0 == '0' // true false == 'false' // false false == '0' // true false == undefined // false false == null // false null == undefined // true ' \t\r\n ' == 0 // true複製程式碼
上面有些看起來會挺困惑的,所以儘量還是使用嚴格比較運運算元(===
)。對於參照型別,==
和===
操作一致(特殊情況除外)。
var a = [1,2,3]; var b = [1,2,3]; var c = { x: 1, y: 2 }; var d = { x: 1, y: 2 }; var e = "text"; var f = "te" + "xt"; a == b // false a === b // false c == d // false c === d // false e == f // true e === f // true複製程式碼
特殊情況是,當你將一個字串字面量與一個字串物件進行比較時,由於該物件的toString
或valueOf
方法,該物件的值與相字面量的值一樣。
考慮將字串字面量與由String
建構函式建立的字串物件進行比較:
"abc" == new String("abc") // true "abc" === new String("abc") // false複製程式碼
在這裡,==
操作符檢查兩個物件的值並返回true
,但是=
==看到它們不是同一型別並返回false
。哪一個是正確的?這取決於你想要比較的是什麼。
我們的建議是完全繞開該問題,只是不要使用String
建構函式來建立字串物件。
使用==運運算元(等於)
true == 1; //true, 因為 true 被轉換為1,然後進行比較 "2" == 2; //true, 因為 「2」 被轉換成 2,然後進行比較複製程式碼
使用===操作符
true === 1; //false "2" === 2; //false複製程式碼
快速克隆,資料丟失– JSON.parse/stringify
如果您沒有在物件中使用Date
、函數、undefined
、Infinity
、RegExp
、Map
、Set
、blob、、稀疏陣列、型別化陣列或其他複雜型別,那麼可以使用一行簡單程式碼來深拷貝一個物件:
JSON.parse(JSON.stringify(object))複製程式碼
const a = { string: 'string', number: 123, bool: false, nul: null, date: new Date(), undef: undefined, // 丟失 inf: Infinity, // 被設定為 null re: /.*/, // 丟失 } console.log(a); console.log(typeof a.date); // object const clone = JSON.parse(JSON.stringify(a)); console.log(clone); /* object { string: 'string', number: 123, bool: false, nul: null, date: '2020-09-04T00:45:41.823Z', inf: null, re: {} } */ console.log(typeof clone.date); // string複製程式碼
使用庫進行可靠的克隆
由於克隆物件不是一件簡單的事情(複雜型別、迴圈參照、函數等等),大多數主要的庫都提供了拷貝物件的函數。如果你已經在使用一個庫,請檢查它是否具有物件克隆功能。例如
lodash – cloneDeep
; 可以通過lodash.clonedeep
模組單獨匯入,如果你尚未使用提供深拷貝功能的庫,那麼它可能是你的最佳選擇
AngularJS – angular.copy
jQuery – jQuery.extend(true, { }, oldObject)
; .clone()
僅克隆DOM元素
ES6
ES6 提供了兩種淺拷貝機制:Object.assign()
和spread
語法。它將所有可列舉的自有屬性的值從一個物件複製到另一個物件。例如
var A1 = {a: "2"}; var A2 = Object.assign({}, A1); var A3 = {...A1}; // Spread Syntax複製程式碼
在以前的測試中,速度是最主要的問題
JSON.parse(JSON.stringify(obj))複製程式碼
這是深拷貝物件的最慢方法,它比jQuery.extend
慢 10-20%。
當deep
標誌設定為false
(淺克隆)時,jQuery.extend
非常快。 這是一個不錯的選擇,因為它包括一些用於型別驗證的額外邏輯,並且不會複製未定義的屬性等,但這也會使你的速度變慢。
如果想拷貝的一個物件且你知道物件結構。那麼,你可以寫一個簡單的for (var i in obj)
迴圈來克隆你的物件,同時檢查hasOwnProperty
,這將比jQuery快得多。
var clonedObject = { knownProp: obj.knownProp, .. }複製程式碼
注意在 Date
物件JSON上使用JSON.parse(JSON.stringify(obj))
方法。JSON.stringify(new Date())
以ISO格式返回日期的字串表示,JSON.parse()
不會將其轉換回Date
物件。
舊版本的JavaScript沒有import
、include
或require
,因此針對這個問題開發了許多不同的方法。
但是從2015年(ES6)開始,JavaScript已經有了ES6模組標準,可以在Node中匯入模組。為了與舊版瀏覽器相容,可以使用Webpack和Rollup之類的構建工具和/或Babel這樣的編譯工具。
ES6 Module
從v8.5開始,Node.js就支援ECMAScript (ES6)模組,帶有--experimental-modules
標誌,而且至少Node.js v13.8.0沒有這個標誌。要啟用ESM(相對於Node.js之前的commonjs風格的模組系統[CJS]),你可以在 package.json
中使用「type」:「module」
。或者為檔案提供擴充套件名.mjs
。(類似地,如果預設為ESM,則用 Node.js 以前的CJS模組編寫的模組可以命名為.cjs
。)
使用package.json
:
{ "type": "module" }複製程式碼
在 module.js:
中
export function hello() { return "Hello"; }複製程式碼
main.js:
import { hello } from './module.js'; let val = hello(); // val is "Hello";複製程式碼
使用.mjs
,會有對應的module.mjs
:
export function hello() { return "Hello"; }複製程式碼
在main.mjs
中
import { hello } from './module.mjs'; let val = hello(); // val is "Hello";複製程式碼
自Safari 10.1,Chrome 61,Firefox 60 和 Edge 16 開始,瀏覽器就已經支援直接載入ECMAScript模組(不需要像Webpack這樣的工具)。無需使用Node.js的.mjs
擴充套件名; 瀏覽器完全忽略模組/指令碼上的副檔名。
<script type="module"> import { hello } from './hello.mjs'; // Or it could be simply `hello.js` hello('world'); </script>複製程式碼
// hello.mjs -- or it could be simply `hello.js` export function hello(text) { const p = document.createElement('p'); p.textContent = `Hello ${text}`; document.body.appendChild(p); }複製程式碼
大家都說簡歷沒專案寫,我就幫大家找了一個專案,還附贈【搭建教學】。
瀏覽器中的動態匯入
動態匯入允許指令碼根據需要載入其他指令碼
<script type="module"> import('hello.mjs').then(module => { module.hello('world'); }); </script>複製程式碼
Node.js require
在 Node.js 中用的較多還是 module.exports/require
// mymodule.js module.exports = { hello: function() { return "Hello"; } }複製程式碼
// server.js const myModule = require('./mymodule'); let val = myModule.hello(); // val is "Hello"
動態載入檔案
我們可以通過動態建立 script
來動態引入檔案:
function dynamicallyLoadScript(url) { var script = document.createElement("script"); document.head.appendChild(script); }複製程式碼
檢測指令碼何時執行
現在,有一個個大問題。上面這種動態載入都是非同步執行的,這樣可以提高網頁的效能。 這意味著不能在動態載入下馬上使用該資源,因為它可能還在載入。
例如:my_lovely_script.js
包含MySuperObject
:
var js = document.createElement("script"); js.type = "text/javascript"; js.src = jsFilePath; document.body.appendChild(js); var s = new MySuperObject(); Error : MySuperObject is undefined複製程式碼
然後,按F5重新載入頁面,可能就有效了。那麼該怎麼辦呢?
我們可以使用回撥函數來解決些問題。
function loadScript(url, callback) { var head = document.head; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; script.onload = callback; head.appendChild(script); }複製程式碼
然後編寫在lambda
函數中載入指令碼後要使用的程式碼
var myPrettyCode = function() { // Here, do whatever you want };複製程式碼
然後,執行程式碼:
loadScript("my_lovely_script.js", myPrettyCode);複製程式碼
請注意,指令碼可能在載入DOM之後或之前執行,具體取決於瀏覽器以及是否包括行script.async = false;
。
相關免費學習推薦:(視訊)
以上就是JavaScript中必須掌握的10個基礎問題的詳細內容,更多請關注TW511.COM其它相關文章!