web前端最新最全面試題總結(JavaScript)
DOM事件模型
DOM事件流
DOM的結構是一個樹形,每當HTML元素產生事件時,該事件就會在樹的根節點和元素節點之間傳播,所有經過的節點都會收到該事件。
DOM事件模型分為兩類:
- 一類是IE所使用的冒泡型事件(Bubbling);
- 另一類是DOM標準定義的冒泡型與捕獲型(Capture)的事件。
- 除IE外的其他瀏覽器都支援標準的DOM事件處理模型。
冒泡型事件處理模型(Bubbling)
冒泡型事件處理模型在事件發生時,首先在最精確的元素上觸發,然後向上傳播,直到根節點。反映到DOM樹上就是事件從葉子節點傳播到根節點。
捕獲型事件處理模型(Captrue)
相反地,捕獲型在事件發生時首先在最頂級的元素上觸發,傳播到最低階的元素上。在DOM樹上的表現就是由根節點傳播到葉子節點。
標準的DOM事件處理模型
標準的事件處理模型分為三個階段:
- 父元素中所有的捕捉型事件(如果有)自上而下地執行
- 目標元素的冒泡型事件(如果有)
- 父元素中所有的冒泡型事件(如果有)自下而上地執行
Event事件的常見api方法
- event.preventDefault(); // 阻止預設事件。
- 阻止冒泡
- w3c的方法:(火狐、谷歌、IE11) event.stopPropagation();
- IE10以下則是: event.cancelBubble = true;
自定義事件
var myEvent = new Event('clickTest');
element.addEventListener('clickTest', function () {
console.log('smyhvae');
});
//元素註冊事件
element.dispatchEvent(myEvent); //注意,引數是寫事件物件 myEvent,不是寫 事件名 clickTest
事件委託(事件代理)
事件委託的概念
事件委託就是利用事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。 事件委託就是方法的代理。 可以簡單理解為自己執行的事件可以讓別的元素代替執行。
**事件委託的原理: **
事件委託是利用事件的冒泡原理來實現的 ,就是事件從最深的節點(目標節點)開始,然後逐步向上傳播事件
事件委託的優點:
- 提高效能:每一個函式都會佔用記憶體空間,只需新增一個事件處理程式代理所有事件,所佔用的記憶體空間更少。
- 動態監聽:使用事件委託可以自動繫結動態新增的元素,即新增的節點不需要主動新增也可以一樣具有和其他元素一樣的事件。
例子解析:
案例:
使用原始方法:
下面的程式碼的意思很簡單,相信很多人都是這麼實現的,我們看看有多少次的dom操作,首先要找到ul,然後遍歷li,然後點選li的時候,又要找一次目標的li的位置,才能執行最後的操作,每次點選都要找一次li;
## 子節點實現相同的功能:
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
## 實現功能是點選li,彈出123:
window.onload = function(){
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
用事件委託的方式
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
}
這裡用父級ul做事件處理,當li被點選時,由於冒泡原理,事件就會冒泡到ul上,因為ul上有點選事件,所以事件就會觸發,當然,這裡當點選ul的時候,也是會觸發的,那麼問題就來了,如果我想讓事件代理的效果跟直接給節點的事件效果一樣怎麼辦,比如說只有點選li才會觸發,不怕,我們有絕招:
Event物件提供了一個屬性叫target,可以返回事件的目標節點,我們成為事件源,也就是說,target就可以表示為當前的事件操作的dom,但是不是真正操作dom,當然,這個是有相容性的,標準瀏覽器用ev.target,IE瀏覽器用event.srcElement,此時只是獲取了當前節點的位置,並不知道是什麼節點名稱,這裡我們用nodeName來獲取具體是什麼標籤名,這個返回的是一個大寫的,我們需要轉成小寫再做比較(習慣問題):
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert(123);
alert(target.innerHTML);
}
}
}
建立物件的常見方式
方式一:字面量
var obj11 = {name: 'smyh'};
var obj12 = new Object(name: `smyh`); //內建物件(內建的建構函式)
# 上面的兩種寫法,效果是一樣的。因為,第一種寫法,obj11會指向Object。
第一種寫法是:字面量的方式。
第二種寫法是:內建的建構函式
方式二:通過建構函式
var M = function (name) {
this.name = name;
}
var obj3 = new M('smyhvae');
方法三:Object.create
var p = {name:'smyhvae'};
var obj3 = Object.create(p); //此方法建立的物件,是用原型鏈連線的
# 這種方式裡,obj3是例項,p是obj3的原型(name是p原型裡的屬性),建構函式是Objecet 。
方式四:通過工廠模式
function createPerson(name,age,job){
var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName=function(){
alert(this.name);
};
return 0;
}
var person1=createPerson("Nicholas",29,"Software Engineer");
var person2=createPerson("Greg",27,"Doctor");
## 與工廠模式相比,具有以下特點:
沒有顯式建立物件;
直接將屬性和方法賦給了this物件;
沒有return語句;
要建立新例項,必須使用new操作符;(否則屬性和方法將會被新增到window物件)
可以使用instanceof操作符檢測物件型別
## 建構函式的問題:
建構函式內部的方法會被重複建立,不同例項內的同名函式是不相等的。可通過將方法移到建構函式外部解決這一問題,但面臨新問題:封裝性不好。
這些問題可通過原型模式解決。
方式五:原型模式
function Person(){
}
Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="...";
Person.prototype.sayName=function(){
...
};
var person1=new Person();
person1.sayName();//"Nicholas"
面向物件:類定義的幾種方式
類的定義/類的宣告
**方式一:**用建構函式模擬類(傳統寫法)
function Animal1() {
this.name = 'smyhvae'; //通過this,表明這是一個建構函式
}
**方式二:**用 class 宣告(ES6的寫法)
class Animal2 {
constructor() { //可以在建構函式裡寫屬性
this.name = name;
}
}
例項化
類的例項化很簡單,直接 new 出來即可。
console.log(new Animal1(),new Animal2()); //例項化。如果括號裡沒有引數,則括號可以省略
面向物件:類的繼承幾種方式
方式一 藉助建構函式
function Parent(){
this.name = 'parent的屬性'
}
function Child(){
Parent.call(this);
this.type = 'child的屬性'
}
console.log(new Child)
【重要】上方程式碼中,最重要的那行程式碼:在子類的建構函式裡寫了Parent1.call(this);
,意思是:讓Parent的建構函式在child的建構函式中執行。發生的變化是:改變this的指向,parent的例項 --> 改為指向child的例項。導致 parent的例項的屬性掛在到了child的例項上,這就實現了繼承。
**缺點:**這種方式,雖然改變了 this 的指向,但是,Child1 無法繼承 Parent1 的原型。也就是說,如果我給 Parent1 的原型增加一個方法,是無法被繼承的。
方式二 通過原型鏈實現繼承
function Parent(){
this.name = 'parent的屬性'
}
function Child(){
this.type = 'child的屬性'
}
Child.prototype = new Parent()
console.log(new Child)
【重要】上方程式碼中,最重要的那行:每個函式都有prototype
屬性,於是,建構函式也有這個屬性,這個屬性是一個物件。現在,我們把Parent的例項賦值給了Child的prototye,從而實現繼承。此時,Child
建構函式、Parent
的例項、Child
的例項構成一個三角關係。於是:
new Child.__proto__ === new Parent()
的結果為true
分析:
這種繼承方式,Child 可以繼承 Parent 的原型,但有個缺點:
缺點是:如果修改 child1例項的name屬性,child2例項中的name屬性也會跟著改變。
方式三:組合的方式:建構函式 + 原型鏈
function Parent3() {
this.name = 'Parent 的屬性';
this.arr = [1, 2, 3];
}
function Child3() {
Parent3.call(this); //【重要1】執行 parent方法
this.type = 'Child 的屬性';
}
Child3.prototype = new Parent3(); //【重要2】第二次執行parent方法
var child = new Child3();
這種方式,能解決之前兩種方式的問題:既可以繼承父類原型的內容,也不會造成原型裡屬性的修改。
這種方式的缺點是:讓父親Parent的構造方法執行了兩次。
方式四:es6中class用extends實現繼承
class Person {
constructor(skin, language) {
this.skin = skin;
this.language = language;
}
say() {
console.log('I am a Person, ' + 'skin: ' + this.skin + 'language: ' + this.language)
}
}
class American extends Person {
aboutMe() {
console.log(this.skin + ' ' + this.language)
}
}
var american = new American('yello', 'chinese')
console.log(american.skin) // yello
american.say() // I am a Person, skin: yellolanguage: chinese
american.aboutMe() // yello chinese
js的執行機制
程式、程序、執行緒
-
程式:由原始碼生成的可執行應用。 (例如:QQ.app)
-
程序:一個正在執行的程式可以看做一個程序,(例如:正在執行的QQ警示),程序擁有獨立執行所需要的全部資源
-
執行緒:程式中獨立執行的程式碼段。(例如:接收QQ訊息的程式碼)
一個程序是由一或多個執行緒組成,程序只負責資源的排程和分配,執行緒才是程式真正的執行單元,負責程式碼的執行。
單執行緒、多執行緒
單執行緒
- 每個正在執行的程式(即程序),至少包括一個執行緒,這個執行緒叫主執行緒
- 主執行緒在程式啟動時被建立,用於執行main函式
- 只有一個主執行緒的程式,稱作單執行緒程式
- 主執行緒負責執行程式的所有程式碼(UI展現以及重新整理,網路請求,本地儲存等等)。這些程式碼只能順序執行,無法併發執行
多執行緒
- 擁有多個執行緒的程式,稱作多執行緒程式。
- iOS允許使用者自己開闢新的執行緒,相對於主執行緒來講,這些執行緒,稱為子執行緒
- 可以根據需要開闢若干子執行緒
- 子執行緒和主執行緒都是獨立的執行單元,各自的執行互不影響,因此能夠併發執行
單執行緒、多執行緒的區別
單執行緒程式:只有一個執行緒,程式碼順序執行,容易出現程式碼阻塞(頁面假死)
多執行緒程式:有多個執行緒,執行緒間獨立執行,能有效地避免程式碼阻塞,並且提高程式的執行效能
案例:
**多執行緒與單執行緒的區別**
生活舉例
你早上上班,正要打卡的時候,手機響了。。你如果先接了電話,等接完了,在打卡,就是單執行緒。
如果你一手接電話,一手打卡。就是多執行緒。
2件事的結果是一樣的。。你接了電話且打了卡。
JS的非同步和單執行緒
因為是單執行緒,所以必須非同步。
非同步的經典案例一:
# 現有如下程式碼:
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
console.log(3);
console.log(4);
# 上面的程式碼中,我們很容易知道,列印的順序是1,3,4,2。因為你會想到,要等一秒之後再列印2。
# 可如果我把延時的時間從1000改成0:
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
console.log(3);
console.log(4);
# 上方程式碼中,列印的順序仍然是1,3,4,2。這是為什麼呢?我們來分析一下。
# 總結:
# js 是單執行緒(同一時間只能做一件事),而且有一個任務佇列:全部的同步任務執行完畢後,再來執行非同步任務。第一行程式碼和最後一行程式碼是同步任務;但是,setTimeout是非同步任務。
於是,執行的順序是:
先執行同步任務console.log(1)
遇到非同步任務setTimeout,要掛起
執行同步任務console.log(3)
全部的同步任務執行完畢後,再來執行非同步任務console.log(2)。
很多人會把這個題目答錯,這是因為他們不懂 js 的執行機制。
注意上面那句話:同步任務執行完畢後,再來執行非同步任務。也就是說,如果同步任務沒有執行完,非同步任務是不會執行的。為了解釋這句話,我們來看下面這個例子。
同步的經典案例一:
console.log('A');
alert('haha'); //1秒之後點選確認
console.log('B');
# alert函式是同步任務,我只有點選了確認,才會繼續列印B。
同步和非同步的對比
我們在上面列舉了非同步和同步的例子。現在來描述一下區別:【重要】
因為setTimeout
是非同步任務,所以程式並不會卡在那裡,而是繼續向下執行(即使settimeout設定了倒計時一萬秒);但是alert
函式是同步任務,程式會卡在那裡,如果它沒有執行,後面的也不會執行(卡在那裡,自然也就造成了阻塞)。
前端使用非同步的場景
什麼時候需要等待,就什麼時候用非同步。
- 定時任務:setTimeout(定時炸彈)、setInterval(迴圈執行)
- 網路請求:ajax請求、動態
<img>
載入 - 事件繫結(比如說,按鈕繫結點選事件之後,使用者愛點不點。我們不可能卡在按鈕那裡,什麼都不做。所以,應該用非同步)
- ES6中的Promise
程式碼舉例:
console.log('start');
var img = document.createElement('img');
img.onload = function () {
console.log('loaded');
}
img.src = '/xxx.png';
console.log('end');
上圖中,先列印start
,然後執行img.src = '/xxx.png'
,然後列印end
,最後列印loaded
。
任務佇列
所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
總結:只要主執行緒空了,就會去讀取"任務佇列",這就是JavaScript的執行機制。【重要】
Event Loop (事件迴圈)
主執行緒從"任務佇列"中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event Loop(事件迴圈)。
事件迴圈是如何執行的,事件迴圈器會不停的檢查事件佇列,如果不為空,則取出隊首壓入執行棧執行。當一個任務執行完畢之後,事件迴圈器又會繼續不停的檢查事件佇列,不過在這期間,瀏覽器會對頁面進行渲染。
關於事件迴圈,我們需要記住以下幾點:
- 事件佇列嚴格按照時間先後順序將任務壓入執行棧執行;
- 當執行棧為空時,瀏覽器會一直不停的檢查事件佇列,如果不為空,則取出第一個任務;
- 在每一個任務結束之後,瀏覽器會對頁面進行渲染;
案例1:
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
# 很多人以為上面的題目,答案是0,1,2,3。其實,正確的答案是:3,3,3,3。
案例2:
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
console.log(i);
# 迴圈執行過程中,幾乎同時設定了 3 個定時器,這些定時器都會在 1 秒之後觸發,而迴圈完的輸出是立即執行的.
# 3 -> 3,3,3,即第 1 個 3 直接輸出,1 秒之後,連續輸出 3 個 3。
前端錯誤的分類
- 即時執行錯誤(程式碼錯誤)
- 資源載入錯誤
Class和普通建構函式有何區別
我們經常會用ES6中的Class來代替JS中的建構函式做開發。
- Class 在語法上更加貼合面向物件的寫法
- Class 實現繼承更加易讀、易理解
- 更易於寫 java 等後端語言的使用
- 本質還是語法糖,使用 prototype
JavaScript中如何檢測一個變數是一個String型別?
請寫出函式實現
typeof(obj) === “string”
typeof obj === “string”
obj.constructor === String
請用js去除字串空格?
# 方法一:使用replace正則匹配的方法
去除所有空格: str = str.replace(/s*/g,"");
去除兩頭空格: str = str.replace(/^s*|s*$/g,"");
去除左空格: str = str.replace( /^s*/, “”);
去除右空格: str = str.replace(/(s*$)/g, “”);
str為要去除空格的字串,例項如下:
var str = " 23 23 “;
var str2 = str.replace(/s*/g,”");
console.log(str2); // 2323
# 方法二:使用str.trim()方法
str.trim()侷限性:無法去除中間的空格,例項如下:
var str = " xiao ming ";
var str2 = str.trim();
console.log(str2); //xiao ming
同理,str.trimLeft(),str.trimRight()分別用於去除字串左右空格。
# 方法三:使用jquery,$.trim(str)方法
$.trim(str)侷限性:無法去除中間的空格,例項如下:
var str = " xiao ming ";
var str2 = $.trim(str)
console.log(str2); // xiao ming
js 字串操作函式
我這裡只是列舉了常用的字串函式,具體使用方法,請參考網址。
concat() – 將兩個或多個字元的文字組合起來,返回一個新的字串。
indexOf() – 返回字串中一個子串第一處出現的索引。如果沒有匹配項,返回 -1 。
charAt() – 返回指定位置的字元。
lastIndexOf() – 返回字串中一個子串最後一處出現的索引,如果沒有匹配項,返回 -1 。
match() – 檢查一個字串是否匹配一個正則表示式。
substr() 函式 – 返回從string的startPos位置,長度為length的字串
substring() – 返回字串的一個子串。傳入引數是起始位置和結束位置。
slice() – 提取字串的一部分,並返回一個新字串。
replace() – 用來查詢匹配一個正則表示式的字串,然後使用新字串代替匹配的字串。
search() – 執行一個正則表示式匹配查詢。如果查詢成功,返回字串中匹配的索引值。否則返回 -1 。
split() – 通過將字串劃分成子串,將一個字串做成一個字串陣列。
length – 返回字串的長度,所謂字串的長度是指其包含的字元的個數。
toLowerCase() – 將整個字串轉成小寫字母。
toUpperCase() – 將整個字串轉成大寫字母。
你如何獲取瀏覽器URL中查詢字串中的引數?
function showWindowHref(){
var sHref = window.location.href;
var args = sHref.split('?');
if(args[0] == sHref){
return "";
}
var arr = args[1].split('&');
var obj = {};
for(var i = 0;i< arr.length;i++){
var arg = arr[i].split('=');
obj[arg[0]] = arg[1];
}
return obj;
}
var href = showWindowHref(); // obj
console.log(href['name']); // xiaoming
怎樣新增、移除、移動、複製、建立和查詢節點?
1)建立新節點
createDocumentFragment() //建立一個DOM片段
createElement() //建立一個具體的元素
createTextNode