常見的 JavaScript 面試問題和答案

已發表: 2023-01-13

在你的投資組合中加入 JavaScript 會增加獲得軟件開發人員角色的機會。 也就是說,讓我們看看常見的 JavaScript 面試問題。

JavaScript 是 Web 開發中最常用的語言之一。 現在幾乎可以用它來開發任何類型的應用程序。

在進入面試問題之前,讓我們看看學習 JavaScript 的優勢。

JavaScript

JavaScript 是一種輕量級、解釋型或即時編譯型編程語言。 它是萬維網的核心語言之一。 你知道 www 的其他兩種核心語言。 如果你不這樣做,你最好搜索它們。

JavaScript 主要是為網絡創建的。 但它現在不僅適用於網絡。 借助Node、Deno等環境,我們幾乎可以在任何平台上運行它。

讓我們來看看它的一些優點。

JavaScript 的優點

  1. 易於上手。 即使沒有任何編碼知識,您也可以學習它。
  2. 周圍的大型社區。 如果您被困在任何地方,您將獲得所需的所有幫助。
  3. 有很多使用 JavaScript 構建的庫/框架,這有助於更快地開發應用程序。
  4. 我們可以使用 JavaScript 開發前端、後端、android、iOS 等應用程序。 我們可以用它創建幾乎任何類型的應用程序。 但是,它在 Web 開發中更強大。

JavaScript 中的數據類型有哪些?

數據類型用於存儲不同類型的數據。 一種編程語言與另一種編程語言的數據類型不同。 在 JavaScript 中,我們有 8 種數據類型。 讓我們一一看看。

  • 數字
  • 細繩
  • 布爾值
  • 不明確的
  • 無效的
  • 大整數
  • 象徵
  • 目的

Object之外的所有數據類型都稱為原始值。 而且它們是不可變的。

JavaScript 中有哪些內置方法?

JavaScript 中的內置方法因數據類型而異。 我們可以使用相應的數據類型訪問這些內置方法。 讓我們看看一些針對不同數據類型和數據結構的內置方法。

  1. 數字
    • 到固定
    • 到字符串
    • ……
  2. 細繩
    • 小寫
    • 以。。開始
    • 圖表在
    • ……
  3. 大批
    • 篩選
    • 地圖
    • 為每個
    • ……

每種數據類型都有很多內置方法。 您可以檢查不同數據類型和數據結構的所有內置方法的引用。

如何在 JavaScript 中創建數組?

數組是 JavaScript 中的核心數據結構之一。 數組可以包含任何類型的數據,因為 JavaScript 是動態的。 讓我們看看如何在 JavaScript 中創建數組。

我們可以使用方括號[]創建一個數組。 創建對像簡單快捷

// Empty array const arr = []; // Array with some random values const randomArr = [1, "One", true]; console.log(arr, randomArr);

我們可以使用Array構造函數創建一個數組。 人們在一般項目中很少使用構造函數來創建數組。

 // Empty array const arr = new Array(); // Array with some random values const randomArr = new Array(1, "One", true); console.log(arr, randomArr);

JavaScript 數組是可變的,即我們可以在創建它們後隨意修改它們。

如何在 JavaScript 中創建對象?

除了數組,對像是 JavaScript 中的另一個核心數據結構。 對象正在使用存儲鍵值對。 鍵必須是一個不可變的值,而值可以是任何東西。 讓我們看看如何在 JavaScript 中創建對象。

我們可以使用大括號{}創建對象。 創建對像簡單快捷。

 // Empty object const object = {}; // Object with some random values const randomObject = { 1: 2, one: "Two", true: false }; console.log(object, randomObject);

我們可以使用Object構造函數創建對象。 人們很少在一般項目中使用它。

 // Empty object const object = new Object(); // Object with some random values const randomObject = new Object(); randomObject[1] = 2; randomObject["one"] = "Two"; randomObject[true] = false; console.log(object, randomObject);

JavaScript 對像是可變的,即我們可以在創建後修改它們,如您在第二個示例中所見。

你如何調試 JavaScript 代碼?

調試代碼並不簡單。 從一種編程語言到另一種編程語言,從一個項目到另一個項目,等等……都是不同的。 讓我們看看用於調試 JavaScript 的常見事物。

#1。 記錄

我們可以在代碼中的多個位置使用console.log語句來識別錯誤。 當上一行中存在錯誤時,代碼將停止運行下一行代碼。

日誌記錄是一種古老的調試方法,對於小型項目非常有效。 這是任何編程語言的常用調試技術。

#2。 開發者工具

JavaScript 主要用於開發 Web 應用程序。 因此,現在幾乎所有瀏覽器都有開發人員工具來幫助調試 JavaScript 代碼。

最常用的調試方法之一是在開發人員工具中設置斷點。 斷點會停止 JavaScript 的執行並提供有關當前執行的所有信息。

我們可以在出現錯誤的地方設置多個斷點,看看是什麼原因造成的。 這是調試 JavaScript Web 應用程序的最有效方法。

#3。 集成開發環境

我們可以使用 IDE 來調試 JavaScript。 VS Code 支持斷點調試。 調試功能可能因您使用的 IDE 而異。 但是,大多數 IDE 都具有該功能。

如何在 HTML 文件中添加 JavaScript 代碼?

我們可以使用script標籤添加 JavaScript HTML 文件。 您可以查看下面的示例。

 <!DOCTYPE html> <html lang="en"> <head> <title>Geekflare</title> </head> <body> <h1>Geekflare</h1> <script> // JavaScript code goes here console.log("This is JavaScript code"); </script> </body> </html>

什麼是 cookie?

Cookie 是用於存儲小信息的鍵值對。 信息可以是任何東西。 我們可以設置cookies的過期時間,過期後會被刪除。 這些被廣泛用於存儲用戶的信息。

即使我們刷新頁面,Cookie 也不會被清除,直到我們刪除它們或它們過期為止。 您可以通過打開開發者工具在任何瀏覽器中查看任何網絡應用程序/網頁的 cookie。

如何讀取cookie?

我們可以使用document.cookie在 JavaScript 中讀取 cookie。 它將返回我們創建的所有 cookie。

 console.log("All cookies", document.cookie);

如果沒有 cookie,它將返回一個空字符串。

如何創建和刪除cookie?

我們可以通過將鍵值對設置為document.cookie來創建 cookie。 讓我們看一個例子。

 document.cookie = "one=One;";

在上面的語法中, one cookie 鍵和One是它的值。 我們可以向 cookie 添加更多屬性,如域、路徑、過期時間等。 它們中的每一個都應該用分號 (;) 分隔。 所有屬性都是可選的。

讓我們看一個帶有屬性的例子。

 document.cookie = "one=One;expires=Jan 31 2023;path=/;";

在上面的代碼中,我們為 cookie 添加了一個過期日期和路徑。 如果未提供到期日期,cookie 將在會話結束後刪除。 默認路徑將是文件路徑。 到期日期格式應為 GMT。

讓我們看看如何創建多個 cookie。

 document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "two=Two;expires=Jan 31 2023;path=/;"; document.cookie = "three=Three;expires=Jan 31 2023;path=/;";

如果在設置多個 cookie 時密鑰或路徑不同,則 cookie 不會被覆蓋。 如果 key 和 path 相同,那麼它將覆蓋之前的 cookie。 查看下面的示例,它將覆蓋之前設置的 cookie。

 document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "one=Two;path=/;";

我們已經從 cookie 中刪除了到期日期並更改了值。

當您測試代碼以使其正常工作時,使用到期日作為未來日期。 如果您保持相同的日期Jan 31 2023即使在Jan 31 2023之後,也不會創建 cookie。

我們已經了解瞭如何創建和更新 cookie。 讓我們看看如何刪除 cookie。

刪除 cookie 很容易。 只需將 cookie 的到期日期更改為任何過去的日期。 檢查下面的例子。

 // Creating cookies document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "two=Two;expires=Jan 31 2023;path=/;"; document.cookie = "three=Three;expires=Jan 31 2023;path=/;"; // Deleting the last cookie document.cookie = "three=Three;expires=Jan 1 2023;path=/;";

您不會在 cookies 中找到最後一個 cookie,因為它已在代碼的最後一行中刪除。 這就是 min cookies 教程。

有哪些不同的 JavaScript 框架?

那裡有很多 JavaScript 框架。 React、Vue、Angular 等,用於 UI 開發。 用於服務器端開發的 Express、Koa、Nest 等。 NextJS、Gatsby 等,用於靜態站點生成。 React Native、Ionic 等,用於移動應用程序開發。 我們在這裡提到了一些 JavaScript 框架。 您可以找到更多需要花費大量時間探索的框架。 在需要時進行探索。

JavaScript 中的閉包

閉包是一個與其詞法作用域及其父詞法環境捆綁在一起的函數。 使用閉包,我們可以訪問外部範圍的數據。 閉包是在創建函數時形成的。

 function outer() { const a = 1; function inner() { // We can access all the data from the outer function scope here // The data will be available even if we execute this function outside the outer function // as inners' closure formed while creating it console.log("Accessing a inside inner", a); } return inner; } const innerFn = outer(); innerFn();

閉包在 JavaScript 應用程序中被廣泛使用。 您可能以前使用過它們而沒有意識到它們是閉包。 關於閉包,要了解的遠不止這些。 確保你已經完全了解了這個概念。

在 JavaScript 中提升

提升是 JavaScript 中的一個過程,其中變量、函數和類的聲明在執行代碼之前移動到範圍的頂部。

 // Accessing `name` before declaring console.log(name); // Declaring and initializing the `name` var name = "Geekflare";

如果你運行上面的代碼,你不會看到任何錯誤。 但在大多數語言中,你會得到錯誤。 輸出將是undefined的,因為提升只會將聲明移動到頂部,並且直到第 3 行才會對其進行初始化。

如下將var更改為letconst ,然後再次運行代碼。

 // Accessing `name` before declaring console.log(name); // Declaring and initializing the `name` const name = "Geekflare";

現在,您將收到引用錯誤,指出我們無法在初始化變量之前訪問該變量。

 ReferenceError: Cannot access 'name' before initialization

所以,這裡在ES6中引入了letconst ,在初始化之前是不能訪問的,報錯。 這是因為用letconst聲明的變量將處於臨時死區 (TDZ) 中,直到它被初始化的那一行。 我們無法從 TDZ 訪問變量。

在 JavaScript 中柯里化

柯里化是一種將具有多個參數的函數轉換為具有多個可調用對象的較少參數的技術。 有了它,我們可以將可調用的函數 add(a, b, c, d) 轉換為可調用的 add(a)(b)(c)(d) 。 讓我們看一個如何做的例子。

 function getCurryCallback(callback) { return function (a) { return function (b) { return function (c) { return function (d) { return callback(a, b, c, d); }; }; }; }; } function add(a, b, c, d) { return a + b + c + d; } const curriedAdd = getCurryCallback(add); // Calling the curriedAdd console.log(curriedAdd(1)(2)(3)(4));

我們可以概括getCurryCallback函數,該函數將用於不同的函數以轉換為 currying callables。 您可以參考 JavaScript Info 了解更多詳細信息。

文檔和窗口的區別

window是瀏覽器中最頂層的對象。 它包含有關瀏覽器窗口的所有信息,如歷史記錄、位置、導航器等; 它在 JavaScript 中全球可用。 我們可以直接在我們的代碼中使用它而無需任何導入。 我們可以在沒有window的情況下訪問window對象的屬性和方法window.

documentwindow對象的一部分。 網頁上加載的所有 HTML 都轉換為文檔對象。 文檔對象指的是特殊的HTMLDocument元素,它和所有的HTML元素一樣,會有不同的屬性和方法。

window對象表示瀏覽器窗口,而document表示在該瀏覽器窗口中加載的 HTML 文檔。

客戶端和服務器端的區別

客戶端是指使用應用程序的最終用戶。 服務器端是指部署應用程序的Web服務器。

在前端術語中,我們可以將用戶計算機上的瀏覽器稱為客戶端,將雲服務稱為服務器端。

innerHTML 和 innerText 的區別

innerHTMLinnerText都是 HTML 元素的屬性。 我們可以使用這些屬性更改 HTML 元素的內容。

我們可以將 HTML 字符串分配給innerHTML一個像普通 HTML 一樣呈現的屬性。 檢查下面的例子。

 const titleEl = document.getElementById("title"); titleEl.innerHTML = '<span style="color:orange;">Geekflare</span>';

在您的 HTML 中添加一個帶有 id title的元素,並將上述腳本添加到 JavaScript 文件中。 運行代碼並查看輸出。 您將獲得橙色的Geekflare 。 如果您檢查該元素,它將位於span標籤內。 所以innerHTML將獲取 HTML 字符串並將其呈現為普通 HTML。

另一邊的innerText將採用普通字符串並按原樣呈現。 它不會像innerHTML那樣呈現任何 HTML。 將上面代碼中的innerHTML更改為innerText並檢查輸出。

 const titleEl = document.getElementById("title"); titleEl.innerText = '<span style="color:orange;">Geekflare</span>';

現在,您將看到我們在網頁上提供的確切字符串。

let 和 var 的區別

letvar關鍵字用於在 JavaScript 中創建變量。 ES6 中引入了let關鍵字。

let是塊作用域, var是函數作用域。

 { let a = 2; console.log("Inside block", a); } console.log("Outside block", a);

運行上面的代碼。 您將在最後一行收到錯誤消息,因為我們無法訪問塊外的let a ,因為它是塊作用域的。 現在,將其更改為var並再次運行。

 { var a = 2; console.log("Inside block", a); } console.log("Outside block", a);

您不會收到任何錯誤,因為我們也可以訪問塊外a變量。 現在,讓我們用一個函數替換塊。

 function sample() { var a = 2; console.log("Inside function", a); } sample(); console.log("Outside function", a);

如果你運行上面的代碼,你會得到一個引用錯誤,因為我們不能在函數外訪問var a it,因為它是函數範圍的。

我們可以使用var關鍵字重新聲明變量,但不能使用let關鍵字重新聲明變量。 讓我們看一個例子。

 var a = "Geekflare"; var a = "Chandan"; console.log(a);
 let a = "Geekflare"; let a = "Chandan"; console.log(a);

第一段代碼不會拋出任何錯誤,值a將更改為最新分配的值。 第二段代碼將拋出錯誤,因為我們無法使用let重新聲明變量。

會話存儲和本地存儲的區別

會話存儲和本地存儲用於在用戶的計算機上存儲信息,這些信息可以在沒有互聯網的情況下訪問。 我們可以將鍵值對存儲在會話存儲和本地存儲中。 如果您提供任何其他數據類型或數據結構,鍵和值都將轉換為字符串。

會話結束後(瀏覽器關閉時),會話存儲將被清除。 在我們清除之前,位置存儲不會被清除。

我們可以分別使用sessionStoragelocalStorage對象訪問、更新和刪除會話存儲和位置存儲。

JavaScript 中的 NaN 是什麼?

NaN縮寫為Not-a-Number 。 它表示某些東西在 JavaScript 中不是合法/有效的數字。 在某些情況下,我們會得到NaN作為輸出,例如0/0undefined * 21 + undefinednull * undefined等。

什麼是詞法作用域?

詞法作用域是指從其父作用域訪問變量。 假設我們有一個具有兩個內部函數的函數。 最裡面的函數可以訪問它的兩個父函數的作用域變量。 同樣,二級函數可以訪問最外層的函數作用域。 讓我們看一個例子。

 function outermost() { let a = 1; console.log(a); function middle() { let b = 2; // `a` are accessible here console.log(a, b); function innermost() { let c = 3; // both `a` and `b` are accessible here console.log(a, b, c); } innermost(); } middle(); } outermost();

當我們在代碼中的某處訪問變量時,JavaScript 使用作用域鏈來查找變量。 首先,它將檢查當前作用域中的變量,然後是父作用域,直到全局作用域。

什麼是按值傳遞和按引用傳遞?

按值傳遞和按引用傳遞是在 JavaScript 中將參數傳遞給函數的兩種方式。

按值傳遞:它創建原始數據的副本並將其傳遞給函數。 因此,當我們對函數進行任何更改時,它不會影響原始數據。 檢查下面的例子。

 function sample(a) { // changing the value of `a` a = 5; console.log("Inside function", a); } let a = 3; sample(a); console.log("Outside function", a);

你會看到a的原始值沒有改變,即使我們在函數內部改變了它。

通過引用傳遞:它將數據的引用傳遞給函數。 因此,當我們對函數進行任何更改時,它也會更改原始數據。

 function sample(arr) { // adding a new value to the array arr.push(3); console.log("Inside function", arr); } let arr = [1, 2]; sample(arr); console.log("Outside function", arr);

當我們在函數內部更改它時,您會看到arr的原始值發生了變化。

注意:所有原始數據類型都是按值傳遞,非原始數據類型是按引用傳遞。

什麼是記憶?

記憶化是一種將計算值存儲在緩存中並在我們再次需要它們時使用它們而無需再次計算它們的技術。 如果計算非常繁重,它將加快代碼的執行速度。 有一個存儲權衡,與時間相比這不是一個大問題。

 const memo = {}; function add(a, b) { const key = `${a}-${b}`; // checking whether we computed the value already or not if (memo[key]) { console.log("Not computing again"); return memo[key]; } // adding the newly computed value to cache // here cache is a simple global object memo[key] = a + b; return memo[key]; } console.log(add(1, 2)); console.log(add(2, 3)); console.log(add(1, 2));

這是一個演示記憶的簡單示例。 在這裡,將兩個數字相加並不是一項繁重的計算。 它只是為了演示。

什麼是其餘參數?

rest 參數用於收集函數中所有剩餘的參數。 假設我們有一個函數,它至少接受 2 個參數,最多可以接受任意數量的參數。 由於我們沒有參數的最大數量,我們可以使用rest operator收集前 2 個參數和普通變量,並使用rest 參數收集所有其他參數。

 function sample(a, b, ...rest) { console.log("Rest parameter", rest); } sample(1, 2, 3, 4, 5);

其餘參數將是上例中最後三個參數的數組。 有了這個,我們可以為一個函數設置任意數量的參數。

一個函數只能有一個剩餘參數。 其餘參數應該是參數順序中的最後一個。

什麼是對象解構?

對象解構用於從對象訪問變量並將它們分配給與對象鍵同名的變量。 讓我們看一個例子。

 const object = { a: 1, b: 2, c: 3 }; // Object destructuring const { a, b, c } = object; // Now, a, b, c will be used as normal variables console.log(a, b, c);

我們可以在同一行中更改解構變量的變量,如下所示。

 const object = { a: 1, b: 2, c: 3 }; // Changing the names of `a` and `b` const { a: changedA, b: changedB, c } = object; // Now, changedA, changedB, c will be used as normal variables console.log(changedA, changedB, c);

什麼是數組解構?

數組解構用於從數組中訪問變量並將它們分配給變量。 讓我們看一個例子。

 const array = [1, 2, 3]; // Array destructuring // It's based on the index of the array const [a, b, c] = array; // Now, we can use a, b, c as normal variables console.log(a, b, c);

什麼是事件捕獲和事件冒泡?

事件捕獲事件冒泡是 HTML DOM 中事件傳播的兩種方式。 假設有兩個 HTML 元素,一個在另一個里面。 一個事件發生在內部元素上。 現在,事件傳播模式將決定這些事件的執行順序。

事件冒泡:它首先在元素上運行事件處理程序,然後是它的元素,然後一直運行到最頂層的元素。 這是所有事件的默認行為。

事件捕獲:我們需要在事件中指定我們需要使用這種類型的事件傳播。 我們可以在添加事件監聽器的時候指定。 如果我們啟用了事件捕獲,事件將按以下順序執行。

  1. 事件從最頂層的元素開始執行,直到目標元素向下。
  2. 目標元素上的事件將再次執行。
  3. 冒泡事件傳播將再次發生,直到最頂層元素啟動。

我們可以通過在事件處理程序中調用event.stopPropogation方法來停止事件傳播。

JavaScript 中的 Promise 是什麼?

Promise對像用於異步操作,這些操作將在未來以成功或失敗狀態完成。

Promise可以處於以下狀態之一。

  1. pending操作仍在進行中。
  2. fulfilled當操作成功完成時。 我們將在成功狀態下獲得結果(如果有的話)。
  3. rejected – 當操作失敗完成時。 我們將知道它失敗的原因(錯誤)。

讓我們看兩個成功和失敗案例的例子。

 // Promise which will complete successfully const successPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve({ message: "Completed successfully" }); }, 300); }); successPromise .then((data) => { console.log(data); }) .catch((error) => { console.log(error); }); // Promise which will complete with failure state const failurePromise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error("Failing the promise for testing")); }, 300); }); failurePromise .then((data) => { console.log(data); }) .catch((error) => { console.log(error); });

如果需要,您可以擁有多個then鏈接。 之前返回的數據將在下一個then回調中被接受。

解釋 JavaScript 中不同類型的作用域

JavaScript 中有兩種類型的作用域。 全局範圍局部範圍

您可能也聽說過函數作用域和塊作用域。 它們分別是varletconst的局部作用域。

什麼是自調用函數?

自調用函數是無名函數,創建後將立即執行。 讓我們看一些例子。

 // Without any parameters (function sayHello() { console.log("Hello, World!"); })(); // With parameters (function add(a, b) { console.log("Sum", a + b); })(1, 2);

我們甚至可以將參數傳遞給您在示例中看到的自調用函數。

什麼是箭頭函數?

箭頭函數是普通函數的語法糖,但有一些變化。 它們在一般用例中表現得像普通函數。 當我們必須有回調時,箭頭函數就派上用場了。 讓我們看看它的語法。

 // arrow functions will return by default if it doesn't have any brackets let add = (a, b) => a + b; console.log(add(1, 2));

箭頭函數和普通函數之間存在一些差異。

  • 箭頭函數沒有自己的this綁定。 箭頭函數中的this關鍵字引用其父作用域this
  • 箭頭函數不能用作構造函數

什麼是回調?

回調是傳遞給在該函數內部調用的另一個函數的函數。 使用回調在 JavaScript 中很常見。 讓我們看一個例子。

 function sample(a, b, callback) { const result = a + b; callback(result); } function finished(result) { console.log("Finished with", result); } sample(1, 2, finished);

finished的函數作為回調傳遞給samplefinished的函數在執行某些操作後用結果調用。 您將看到回調主要用於異步操作,如 promises、setTimeout 等。

有哪些不同類型的錯誤?

讓我們檢查一下 JavaScript 中的一些錯誤。

ReferenceError :如果我們正在訪問的變量可用,則會發生此錯誤。

TypeError:如果錯誤與其他類型的錯誤不匹配,JavaScript 將拋出此錯誤。 當我們嘗試執行與數據不兼容的操作時,也會發生這種情況。

SyntaxError:如果 JavaScript 語法不正確,就會出現這個錯誤。

還有一些其他類型的錯誤。 但這些是 JavaScript 中常見的錯誤類型。

JavaScript 中變量的不同作用域是什麼?

JavaScript 中有兩種變量作用域。 使用var關鍵字聲明的變量將具有函數作用域,而使用letconst聲明的變量將具有塊作用域

有關這些變量範圍的更多詳細信息,請參閱第 17 個問題。

什麼是 JavaScript 中的轉義字符?

反斜杠是 JavaScript 中的轉義符。 用來打印一些我們一般不能打印的特殊字符。 假設我們想在一個字符串中打印apostrophe (') ,但我們通常不能這樣做,因為該字符串將在第二個撇號處結束。 在這種情況下,我們將轉義字符以避免在該點結束字符串。

 const message = 'Hi, I\'m Geekflare'; console.log(message);

我們可以在不使用轉義字符的情況下通過將外部單撇號替換為雙撇號來實現上述輸出。 但這只是一個如何使用轉義字符的例子。 還有其他字符我們肯定需要轉義字符,如\n\t\\等,

什麼是 BOM 和 DOM?

瀏覽器對像模型 (BOM):所有瀏覽器都有代表當前瀏覽器窗口的 BOM。 它包含我們最頂層的窗口對象,用於操作瀏覽器窗口。

文檔對像模型 (DOM):瀏覽器在樹結構中加載 HTML 時創建 DOM。 我們可以使用 DOM API 操作 HTML 元素。

什麼是屏幕對象?

屏幕對像是全局窗口對象的屬性之一。 它包含呈現當前瀏覽器窗口的屏幕的不同屬性。 一些屬性是widthheightorientationpixelDepth等。

結論

以上所有問題可能會有後續問題。 因此,您需要圍繞上述所有問題準備概念。

您還可以探索一些常見的 Java 面試問題和答案。

快樂學習