一、HTML 基礎篇
1、doctype 的作用是什麼?
DOCTYPE 是 html5 标準網頁聲明,且必須聲明在HTML文檔的第一行。來告知浏覽器的解析器用什麼文檔标準解析這個文檔,不同的渲染模式會影響到浏覽器對于 CSS 代碼甚至 JavaScript 腳本的解析。
2、HTML、XHTML、XML 有什麼差別?
HTML(超文本标記語言): 在 html4.0 之前 HTML 先有實作再有标準,導緻 HTML 非常混亂和松散
XML(可擴充标記語言): 主要用于存儲資料和結構,JSON作用類似,但更加輕量高效
XHTML(可擴充超文本标記語言): 基于上面兩者而來
3、HTML 語義化的了解?
語義化:指使用恰當語義的 html 标簽,如 header 标簽 代表頭部,article 标簽代表正文等
好處:增強了可讀性、有利于SEO優化
4、常用的 meta 标簽?
charset,用于描述 HTML 文檔的編碼形式
<meta charset="UTF-8" >
http-equiv,相當于http 的檔案頭作用,比如下面的代碼就可以設定 http 的緩存過期日期
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT">
viewport,控制視口的大小和比例
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
5、script 标簽中 defer 和 async 的差別?
defer:script 被異步加載後并不會立刻執行,而是等待文檔被解析完畢後執行。
async:腳本加載完畢後立即執行
6、前端儲存的方式?
cookies: 相容性好,請求頭自帶 cookie 友善,缺點是大小隻有4k,自動請求頭加入 cookie 浪費流量,每個 domain 限制20個 cookie,使用起來麻煩需要自行封裝
localStorage:HTML5 加入的以鍵值對(Key-Value)為标準的方式,優點是操作友善,永久性儲存(除非手動删除),大小為5M,相容IE8+
sessionStorage:與 localStorage 基本類似,差別是 sessionStorage 當頁面關閉後會被清理,而且與 cookie、localStorage 不同,他不能在所有同源視窗中共享,是會話級别的儲存方式
IndexedDB:NoSQL 資料庫,用鍵值對進行儲存,可以進行快速讀取操作,非常适合 web 場景,同時用 JavaScript 進行操作會非常友善。
二、CSS 篇
1、CSS 盒模型
标準模型:寬高計算不包含 padding 和 border ;通過 box-sizing: content-box; 來設定(浏覽器預設)。
IE模型:寬高計算包含 padding 和 border ;通過 box-sizing: border-box; 來設定。
2、BFC(塊狀格式化上下文)
特點:
是一個獨立的容器,裡面的元素和外面的元素互不影響;
BFC垂直方向的邊距會發生重疊;
BFC 區域不會與浮動元素區域重疊;
計算 BFC 高度時,浮動元素也參與計算。
建立方式:
float 值不為 none;
position 的值不為 static 或 relative;
display 為 inline-box, table, table-cell 等;
overflow 不為 visible
作用:
清除浮動
防止同一 BFC 容器中的相鄰元素間的外邊距重疊問題
3、實作垂直居中布局
寬高固定
div.parent {
position: relative;
}
div.child {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -50px;
margin-top: -50px;
}
或
div.child {
width: 100px;
height: 100px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
寬高不固定
div.parent {
display: flex;
justify-content: center;
align-items: center;
}
或
div.parent{
display:flex;
}
div.child{
margin:auto;
}
或
div.parent {
position: relative;
}
div.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
或
div.parent {
display: grid;
}
div.child {
justify-self: center;
align-self: center;
}
4、分析比較 opacity: 0、visibility: hidden、display: none 優劣和适用場景。
結構上:display:none 會從渲染樹中消失,元素不占據空間且無法點選;visibility: hidden 不會從渲染樹中消失,元素繼續占據空間但無法點選;opacity: 0 不會從渲染樹消失,元素占據空間且可點選。
繼承性:display: none 和 opacity: 0 是非繼承屬性;父元素設定了 display:none 或 opacity: 0,子元素無論怎麼設定都無法顯示;visibility: hidden 會被子元素繼承,并且子元素可以通過設定設定 visibility: visible; 來取消隐藏。
性能:display: none 會引起重排,性能消耗較大;visibility: hidden 會引起重繪,性能消耗相對較小; opacity: 0 會重建圖層,性能較高
5、 link 标簽和 import 标簽的差別
link 屬于html 标簽,而 @import 是 css 提供的;
頁面被加載時,link 會同時被加載,而 @import 引用的 css 會等到頁面加載結束後加載;
link 方式樣式的權重高于 @import 的;
link 可以使用 js 動态引入,@import不行;
link 此沒有相容性要求,而 @import 要求 IE5 以上才能識别。
6、移動端 Retina 1px 像素問題的解決方案
viewport + rem
background-image
僞元素 + transform scale()
box-shadow
7、文本顯示行數控制
單行
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
多行
overflow: hidden;
text-overflow: ellipsis; // 超出顯示'...'
display: -webkit-box; // 将元素作為彈性伸縮盒子模型顯示 。
-webkit-line-clamp: 2; // 用來限制在一個塊元素顯示的文本的行數
-webkit-box-orient: vertical; // 設定或檢索伸縮盒對象的子元素的排列方式
8、清除浮動的方式
(1)
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
(2)clear:both
(3)overflow:hidden
9、transition 和 animate 有何差別?
transition:用于做過渡效果,沒有幀概念,隻有開始和結束狀态,性能開銷較小
animate:用于做動畫,有幀的概念,可以重複觸發且有中間狀态,性能開銷較大
10、實作一個扇形
.sector {
width: 0;
height: 0;
border-width: 50px;
border-style: solid;
border-color: red transparent transparent;
border-radius: 50px;
}
三、JS篇
1、JS 的内置類型
基本類型:null、undefined、boolean、number、string、symbol
對象(Object):引用類型(也稱為複雜類型)
注意: NaN 也屬于 number 類型,并且 NaN 不等于自身。
2、類型判斷
Typeof
console.log(typeof 1); // number
console.log(typeof 'a'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof function fn(){}); // function
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof []); // object
console.log(typeof new Error()); // object
注意:typeof 對于基本類型,除了 null 都可以顯示正确的類型;對于對象,除了函數都會顯示 object
Object.prototype.toString
var number = 1; // [object Number]
var string = '123'; // [object String]
var boolean = true; // [object Boolean]
var und = undefined; // [object Undefined]
var nul = null; // [object Null]
var obj = {a: 1} // [object Object]
var array = [1, 2, 3]; // [object Array]
var date = new Date(); // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g; // [object RegExp]
var func = function a(){}; // [object Function]
function checkType() {
for (var i = 0; i < arguments.length; i++) {
console.log(Object.prototype.toString.call(arguments[i]))
}
}
checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)
3、原型和原型鍊的了解
原型:每個函數都有 prototype 屬性,該屬性指向原型對象;使用原型對象的好處是所有對象執行個體共享它所包含的屬性和方法。
原型鍊:主要解決了繼承的問題;每個對象都擁有一個原型對象,通過__proto__ 指針指向其原型對象,并從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null。
4、執行上下文
全局執行上下文
函數執行上下文
eval執行上下文
5、閉包
閉包是指有權通路另一個函數作用域中的變量的函數。
閉包會使得函數内部的變量都被儲存在記憶體中,造成較大的記憶體開銷,是以不要濫用閉包。解決的方法是在退出函數之前将不使用的局部變量置為 null ;
經典面試題:改造下面的代碼,使之輸出0 - 9
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
方法一、利用 setTimeout 函數的第三個參數,會作為回調函數的第一個參數傳入
for (var i = 0; i < 10; i++) {
setTimeout(i => {
console.log(i);
}, 1000, i)
}
方法二、使用 let 變量 的特性
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000)
}
等價于
for (let i = 0; i < 10; i++) {
let _i = i;// const _i = i;
setTimeout(() => {
console.log(_i);
}, 1000)
}
方法三、利用函數自執行的方式,把目前 for 循環過程中的 i 傳遞進去,建構出塊級作用域。
for (var i = 0; i < 10; i++) {
(i => {
setTimeout(() => {
console.log(i);
}, 1000)
})(i)
}
6、this 指向的問題
this 的指向取決于函數以哪種方式調用:
作用函數調用:非嚴格模式下 this 指向全局對象,嚴格模式為 undefined
作用方法調用:this 指向調用函數的對象
構造函數調用:this 指向 new 建立出來的執行個體對象
call()和apply:它們的第一個參數為 this 的指向
補充:箭頭函數中的 this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
箭頭函數其實是沒有 this 的,這個函數中的 this 隻取決于他外面的第一個不是箭頭函數的函數的 this。在這個例子中,因為調用 a 符合前面代碼中的第一個情況,是以 this 是 window。并且 this 一旦綁定了上下文,就不會被任何代碼改變。
7、call 和 apply 的實作
Function.prototype.call2 = function(context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args + ')');
delete context.fn
return result;
}
Function.prototype.apply = function (context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
}
else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
8、bind 的實作
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new TypeError("error");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
// 通過一個空函數作一個中轉,避免綁定函數的 prototype 的屬性被修改
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
9、new 的實作原理
new 操作符做了什麼?
建立一個空對象
然後讓這個空對象的__proto__指向函數的原型prototype
執行構造函數中的代碼,構造函數中的this指向該對象
如果構造函數有傳回值,則以該對象作為傳回值。若沒有return或return了基本類型,則将新對象作為傳回值
function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
var ret = Constructor.apply(obj, arguments);
return typeof ret === 'object' ? ret : obj;
};
9、instanceof 的實作
function instance_of(L, R) {
//L 表示左表達式,R 表示右表達式
var O = R.prototype; // 取 R 的顯示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) return false;
if (O === L)
// 這裡重點:當 O 嚴格等于 L 時,傳回 true
return true;
L = L.__proto__;
}
}
10、深淺拷貝
淺拷貝——如果被拷貝對象的元素是基本類型,就會拷貝出一份,并且互不影響。而如果被拷貝對象的元素是對象或者數組,就隻會拷貝對象和數組的引用,此時若是在新舊對象上進行修改,都會互相影響。
// 數組淺拷貝:slice()、concat()
// 對象淺拷貝:Object.assign()、ES6的擴充運算符
複制代碼
深拷貝——完全的拷貝一個對象,即使嵌套了對象,兩者也互相分離,修改對象的屬性,也不會影響到另一個。
// 遞歸實作
function clone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
if (typeof source[i] === 'object') {
target[i] = clone(source[i]); // 如果是引用類型,則繼續周遊
} else {
target[i] = source[i];
}
}
}
return target;
}
當然這隻是簡單的實作,沒有考慮到特殊的情況,如對象或數組中的函數,正則等特殊類型的拷貝等。
// JSON.parse(JSON.stringify)
var arr = [
{ value: 1 },
{ value: 2 },
{ value: 3 }
];
var copyArr = JSON.parse(JSON.stringify(arr))
copyArr[0].value = 0;
console.log(arr); // [{value: 1}, { value: 2 }, { value: 3 }]
console.log(copyArr); // [{value: 0}, { value: 2 }, { value: 3 }]
上面這種方法簡單粗暴,缺點是不能拷貝函數。
11、防抖和節流的實作(簡易版)
防抖:觸發高頻事件後n秒内函數隻會執行一次,如果n秒内高頻事件再次被觸發,則重新計算時間
funtion debounce(fn) {
// 建立一個标記用來存放定時器的傳回值
let timeout = null;
return function() {
// 每次觸發事件時都取消之前的延時調用方法
clearTimeout(timeout);
// 然後又建立一個新的 setTimeout, 這樣就能保證 1000ms 間隔内如果重複觸發就不會執行 fn 函數
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, 1000);
};
}
節流:高頻事件觸發,但在n秒内隻會執行一次,是以節流會稀釋函數的執行頻率
function throttle(fn) {
// 通過閉包儲存一個标記
let canRun = true;
return function(){
// 每次開始執行函數時都先判斷标記是否為 true,不為 true 則 return
if (!canRun) return;
// 上一次定時器執行完後 canRun 為 true,是以要先設定為false
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
// 最後在 setTimeout 執行完畢後再把标記設定為true(關鍵)表示可以執行下一次循環了。當定時器沒有執行的時候标記永遠是 false,在開頭被 return 掉
canRun = true;
}, 1000)
}
}
12、ES5繼承的實作
// 組合繼承
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
13、JS 異步解決方案的發展曆程以及優缺點
回調函數(callback)
ajax('XXX1', () => {
// callback 函數體
ajax('XXX2', () => {
// callback 函數體
ajax('XXX3', () => {
// callback 函數體
})
})
})
優點:解決了同步的問題
缺點:回調地獄,不能用 try catch 捕獲錯誤,不能 return
Promise
ajax('XXX1')
.then(res => {
// 操作邏輯
return ajax('XXX2')
}).then(res => {
// 操作邏輯
return ajax('XXX3')
}).then(res => {
// 操作邏輯
})
優點:解決了回調地獄的問題
缺點:無法取消 Promise ,錯誤需要通過回調函數來捕獲
Generator
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
// 配合 co 庫使用
const co = require('co')
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
}
co(fetch()).then(data => {
//code
}).fetch(err => {
//code
})
優點:可以控制函數的執行,配合自動執行器 co 子產品 簡化了手動執行的步驟
缺點:不配合 co 函數庫的話使用起來比較麻煩
async/await
// async其實是一個文法糖,它的實作就是将 Generator 函數和自動執行器(co),包裝在一個函數中
async function test() {
// 以下代碼沒有依賴性的話,完全可以使用 Promise.all 的方式
// 如果有依賴性的話,其實就是解決回調地獄的例子了
await fetch('XXX1')
await fetch('XXX2')
await fetch('XXX3')
}
read().then((data) => {
//code
}).catch(err => {
//code
});
優點:代碼清晰,不用像 Promise 寫一大堆 then 鍊,處理了回調地獄的問題
缺點:await 将異步代碼改造成同步代碼,如果多個異步操作沒有依賴性而使用 await 會導緻性能上的降低。
14、setTimeout、Promise、Async/Await 的差別
setTimeout —— setTimeout的回調函數會放到宏任務隊列裡,等到執行棧清空以後執行
console.log('script start') //1. 列印 script start
setTimeout(function() {
console.log('settimeout') // 4. 列印 settimeout
}) // 2. 調用 setTimeout 函數,并定義其完成後執行的回調函數
console.log('script end') //3. 列印 script start
// 輸出順序:script start->script end->settimeout
Promise —— Promise本身是同步的立即執行函數, 當在 executor 中執行resolve或者reject的時候, 此時是異步操作, 會先執行then/catch等,當主棧完成後,才會去調用resolve/reject中存放的方法執行。
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function() {
console.log('settimeout')
})
console.log('script end')
// 輸出順序: script start->promise1->promise1 end->script end->promise2->settimeout
async/await —— async 函數傳回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先傳回,等到觸發的異步操作完成,再執行函數體内後面的語句。可以了解為,是讓出了線程,跳出了 async 函數體。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 輸出順序:script start->async1 start->async2->script end->async1 end
15、Promise的簡單實作
promise 的使用(有關 Promise 的詳細用法,可參考 阮一峰老師的ES6文檔)
var promise = new Promise((resolve,reject) => {
if (操作成功) {
resolve(value)
} else {
reject(error)
}
})
promise.then(function (value) {
// success
},function (value) {
// failure
})
簡單實作
function myPromise(constructor) {
let self = this;
self.status = "pending" // 定義狀态改變前的初始狀态
self.value = undefined; // 定義狀态為resolved的時候的狀态
self.reason = undefined; // 定義狀态為rejected的時候的狀态
function resolve(value) {
if(self.status === "pending") {
self.value = value;
self.status = "resolved";
}
}
function reject(reason) {
if(self.status === "pending") {
self.reason = reason;
self.status = "rejected";
}
}
// 捕獲構造異常
try {
constructor(resolve,reject);
} catch(e) {
reject(e);
}
}
添加 then 方法
myPromise.prototype.then = function(onFullfilled,onRejected) {
let self = this;
switch(self.status) {
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
var p = new myPromise(function(resolve,reject) {
resolve(1)
});
p.then(function(x) {
console.log(x) // 1
})
16、前端子產品化發展曆程
IIFE: 使用自執行函數來編寫子產品化,特點:在一個單獨的函數作用域中執行代碼,避免變量沖突。
(function(){
return {
data:[]
}
})()
AMD: 使用 requireJS 來編寫子產品化,特點:依賴必須提前聲明好。
define('./index.js',function(code){
// code 就是index.js 傳回的内容
})
CMD: 使用 seaJS 來編寫子產品化,特點:對于依賴的子產品是延遲執行,依賴可以就近書寫,等到需要用這個依賴的時候再引入這個依賴,支援動态引入依賴檔案。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自帶的子產品化。
var fs = require('fs');
ES Modules: ES6 引入的子產品化,支援 import 來引入另一個 js 。
import a from 'a';
17、ES6 子產品和 CommonJS 子產品的差異?
ES6子產品在編譯時,就能确定子產品的依賴關系,以及輸入和輸出的變量;CommonJS 子產品,運作時加載。
ES6 子產品自動采用嚴格模式,無論子產品頭部是否寫了 "use strict";
require 可以做動态加載,import 語句做不到,import 語句必須位于頂層作用域中。
ES6 子產品中頂層的 this 指向 undefined,CommonJS 子產品的頂層 this 指向目前子產品。
CommonJS 子產品輸出的是一個值的拷貝,ES6 子產品輸出的是值的引用。