Day3 - 前端高頻面試題之基礎版

傳送門>>>

上一篇: Day2 - 前端高頻面試題之基礎版javascript

下一篇: Day4 - 前端高頻面試題之瀏覽器相關html


一、什麼是變量提高?var、let 及 const 區別?什麼是暫時性死區?

變量提高是將變量聲明提高到做用域頂部,函數也能夠被提高,而且優先於變量提高前端

// var 存在提高,能在聲明以前使用 
console.log(a)	// undefined
var a = 1
複製代碼
// 函數提高是把整個函數提高到做用域頂部
console.log(a) // ƒ a() {}
function a() {}
var a = 1
複製代碼

在全局做用域下使用 let 和 const 聲明變量,變量不會被掛載到 頂層對象window或者global的屬性上java

console.log(b) // 報錯
let b = 1
const c = 1
window.b // undefined
window.c // undefined

-------

// let 聲明的變量僅在塊級做用域內有效
{
  let a = 10;
}
a // ReferenceError: a is not defined.

// let 也不容許在相同做用域內,重複聲明同一個變量
複製代碼

let 實際上爲 JavaScript 新增了塊級做用域node

爲何須要塊級做用域?jquery

ES5 只有全局做用域和函數做用域,沒有塊級做用域,這帶來不少不合理的場景es6

一、內層變量可能會覆蓋外層變量面試

二、用來計數的循環變量泄露爲全局變量編程

在代碼塊內,使用let和const命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」json

在沒有let以前,typeof運算符是百分之百安全的,永遠不會報錯。如今這一點不成立了。這樣的設計是爲了讓你們養成良好的編程習慣,變量必定要在聲明以後使用,不然就報錯。

typeof undeclared_variable // "undefined"
typeof x; // ReferenceError
let x;

---

function bar(x = y, y = 2) {
  return [x, y];
}
bar(); // 報錯 由於參數x默認值等於另外一個參數y,而此時y尚未聲明,屬於「死區」
複製代碼

const聲明一個只讀的常量。一旦聲明,常量的值就不能改變;

實際上,並非變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。

對於簡單數據類型來講,就是常量;對於複合數據類型來講(主要是對象和數組),變量指向的內存地址,保存的是一個指向實際數據的指針,const只能保證這個指針是固定的(即老是指向另外一個固定的地址),至於它指向的數據結構是否是可變的,就徹底不能控制了。

const a = {
  name: 'xyz',
  age: 18
}

// 這裏至關於修改指針指向的值
a.name = 'abc'
console.log(a)	// {name: 'abc', age: 18}

// 將 a 指向另外一個對象,就會報錯
a = {}	// Uncaught TypeError: Assignment to constant variable.

----

const a = [];
a.push('Hello'); // 可執行
a.length = 0;    // 可執行
a = ['Dave'];    // 報錯
複製代碼

二、如何判斷一個對象是否爲空

// 1.將json對象轉化爲json字符串,再判斷該字符串是否爲"{}"
var data = {};
var b = (JSON.stringify(data) == "{}");
alert(b);//true


// 2.for in 循環判斷
var obj = {};
var b = function() {
    for(var key in obj) {
        return false;
    }
    return true;
}
alert(b());//true


// 三、jquery的isEmptyObject方法
此方法是jquery將2方法(for in)進行封裝,使用時須要依賴jquery
var data = {};
var b = $.isEmptyObject(data);
alert(b);//true


// 4.Object.getOwnPropertyNames()方法
此方法是使用Object對象的getOwnPropertyNames方法,
獲取到對象中的屬性名,存到一個數組中,返回數組對象,
咱們能夠經過判斷數組的length來判斷此對象是否爲空
注意:此方法不兼容ie8,其他瀏覽器沒有測試
var data = {};
var arr = Object.getOwnPropertyNames(data);
alert(arr.length == 0);//true


// 5.使用ES6的Object.keys()方法
// 與4方法相似,是ES6的新方法, 返回值也是對象中屬性名組成的數組
var data = {};
var arr = Object.keys(data);
alert(arr.length == 0);//true

複製代碼

三、能夠說一說call,apply,bind嗎?

  • 都是用來改變 this指向的;第一個參數都是 this 要指向的對象,也就是想指定的上下文;
  • call和apply是直接執行函數。call的第二部分參數要一個一個傳,apply要把這些參數放到數組中;
  • bind 返回的是一個新的函數,你必須調用它纔會被執行。
// call 和 apply 是爲了動態改變 this 而出現的
function Fruits() {}
Fruits.prototype = {
  color: 'red',
  getColor: function() {
    return 'color is' + this.color
  }
}
var apple = new Fruits()
apple.getColor()	// color is red

var banana = {
  color: 'yellow'
}

apply.getColor.call(banana)	// color is yellow
apply.getColor.apply(banana)	// color is yellow

// 當一個 object 沒有某個方法(本栗子中banana沒有say方法),
// 可是其餘的有(本栗子中apple有say方法),咱們能夠藉助call或apply用其它對象的方法來操做

複製代碼

// 實例:
var  numbers = [5, 458 , 120 , -215]
Math.max.apply(null, numbers)	// 458
Math.min.call(null, 5, 458 , 120 , -215)	// -215

複製代碼

// 面試題:
// 定義一個 log 方法,讓它能夠代理 console.log 方法,常見的解決方法是:

function log(){
  console.log.apply(null, arguments)	// 第一個參數爲null,表明指向全局對象window或者global
}
log(1, 'www', '&&&')	// 1 "www" "&&&"

// 進階:開頭加上(app):
function log(){
  var args = Array.prototype.slice.call(arguments)	// 須要將僞數組轉化爲標準數組
	args.unshift('(app)')
	console.log.apply(null, args)
}
log(1, '***')	// (app) 1 ***


// 僞數組轉化爲標準數組
1Array.prototype.slice.call(arguments)

2var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(slice)
slice(arguments)	// arguments轉換成了標準數組

複製代碼

四、能說一下AMD、CMD、CommonJSES6的區別嗎?

(1)對於AMD(異步模塊定義)

RequireJS在推廣過程當中對模塊定義的規範化產出

特色 :提早執行,推崇依賴前置;異步加載,不阻塞頁面加載,能並行加載多個模塊,可是不能按需加載

重要的API:

define(id?,[]?,callbakc) // 定義聲明模塊 (模塊id標識(可選),數組 依賴的其餘模塊(可選),回調函數)

require([module],callback) // 加載模塊 (數組 指定加載的模塊,回調函數)

還有一個配置屬性API require.config()

簡單的用法:AMD規範使用define方法定義模塊

// 主入口 index.js
require.config({
    baseUrl: '',
    map: {},
    paths: {
        "jquery": "../js/jquery-1.11.1.min",
        "validate": "../js/jquery.validate.min",
        "moduleTest":"test" //自定義AMD 模式的模塊
    },   // 對外加載的模塊名稱
    shim: {
        'jquery.validate': ['jquery'],  // 配置 jquery 依賴
        'validate.form': ['jquery', 'validate']
    }  // 配置非AMD模式的文件
})


// 單個模塊 test.js 
define([
    'jquery',
    'validate'
], function(_, _validate) {
    console.log(_)
    return {
        add: function (x, y) {
            return x + y
        }
    }
})


// 加載test模塊 
require(['moduleTest'], function(test) {
    // 依賴前置 就是依賴必須一開始就寫好,即便在最後用到
    // …… doSomething()
    test.add(1, 2)
})

複製代碼

(2)對於CMD

Sea.js在推廣過程當中對模塊定義的規範化產出

特色:延遲執行,推崇依賴就近;按需加載,不須要開始就加載全部的模塊,一個模塊就是一個JS文件

define(function(require, exports, module) {
    let a = require('../a')
    a.doSomething()

    // 依賴就近 就是在使用前一步引入就能夠
    let b = require('../b')
    b.doSomething()
})
複製代碼

因爲Node.js主要用於服務器編程,文件在本地,加載都比較快,通常不用考慮非同步的狀況,因此CommonJS規範比較適合

而在瀏覽器端,要從服務器加載模塊,就須要採用非同步方式,所以通常使用AMD/CMD規範

(3)對於CommonJS規範

CommonJS模塊就是對象,每一個文件就是一個模塊,有本身的做用域;

特色:

「運行時加載」,全部代碼都運行在模塊做用域,不會污染全局做用域

模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存

模塊加載的順序,按照其在代碼中出現的順序

  • 模塊導出(module、module.exports

每一個模塊內部,module表明當前模塊,是一個對象,它的exports屬性(即module.exports)是對外的接口,其餘文件加載該模塊,實際上就是讀取module.exports 變量;

爲了方便,Node爲每一個模塊提供一個exports變量,指向module.exports,即let exports = module.exports

因此在對外輸出模塊接口時,能夠向exports對象添加方法;

注意:不能直接將exports變量指向一個值,由於這樣等於切斷了exportsmodule.exports的聯繫

// lib.js

var counter = 3;
function incCounter() {}

// 單個導出
module.exports.counter = counter
module.exports.incCounter = incCounter


// 或者導出一個對象
module.exports = {
  counter: counter,
  incCounter: incCounter,
}
複製代碼
  • 模塊導入

require命令用於加載模塊文件,腳本代碼在require的時候,就會所有執行;

使用require屢次加載同一個模塊時,只會在加載第一次時執行一次,後面再加載,都是直接取第一次運行的結果,除非手動清除系統緩存;

CommonJS模塊的加載機制是,輸入的是被輸出的值的拷貝。也就是一旦輸出一個值,模塊內部的變化就影響不到這個值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
// 輸出內部變量counter和改寫這個變量的內部方法incCounter
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

複製代碼

(4)對於ES6

能夠取代 CommonJSAMD規範,成爲瀏覽器和服務器通用的模塊解決方案。

ES6 模塊不是對象,而是經過export命令顯式指定輸出的代碼,再經過import命令輸入

特色:

它是編譯時加載」或者靜態加載;

import 會自動提高到代碼的頂層;

輸出的是值的引用,即原始值變化,import加載的值也會發生變化;

exportimport 只能出如今模塊的頂層;

ES6 模塊之中,頂層的this指向undefined,即不該該在頂層代碼使用this

模塊功能主要由兩個命令構成:export 用於規定模塊的對外接口 和 import用於引入其餘模塊提供的功能

  • export 命令
// export.js 能夠對外輸出常量、方法和類

// 變量
export let a = 'xyz'
// 函數
export function fn () {}
// 類
export class class1 {}


// 須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係
export 1;	// 報錯

var m = 1;
export m;	// 報錯

// 正確寫法
// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};

複製代碼
  • import命令

使用export命令定義了模塊的對外接口之後,其餘 JS 文件就能夠經過import命令加載這個模塊

大括號裏面的變量名,必須與被導入模塊(export.js)對外接口的名稱相同。

import命令是編譯階段執行的,在代碼運行以前,因此會被提高到模塊的頭部,首先執行

// import命令輸入的變量都是隻讀的,由於它的本質是輸入接口。也就是說,不容許在加載模塊的腳本里面,改寫接口

// 靜態加載,只加載export.js 文件中兩個變量,其餘不加載
import {a, fn} from './export.js'

a=22 // Syntax Error : 'a' is read-only
a.name = 'xyz'	// 若是a是一個對象,改寫a的屬性是容許的


//import命令要使用as關鍵字,將輸入的變量重命名
import {fn as fn1} from './export.js'

// 總體加載
import * as all from './export.js'

複製代碼

目前階段,經過 Babel 轉碼,CommonJS 模塊的require命令和 ES6 模塊的import命令,能夠寫在同一個模塊裏面,可是最好不要這樣作。由於import在靜態解析階段執行,因此它是一個模塊之中最先執行的。

  • export default 命令 模塊指定默認輸出

本質上,export default就是輸出一個叫作default的變量或方法

// 第一組
export default function crc32() { // 輸出
  // ...
}

import crc32 from 'crc32'; // 輸入

// 第二組
export function crc32() { // 輸出
  // ...
};

import {crc32} from 'crc32'; // 輸入

// 第一組是使用export default時,對應的import語句不須要使用大括號;
// 第二組是不使用export default時,對應的import語句須要使用大括號。

複製代碼

正是由於export default命令其實只是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句。

// 正確
export var a = 1;

// 正確
var a = 1;
export default a;	// 將變量a的值賦給變量default

// 錯誤
export default var a = 1;	
// export default命令是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句

// 正確
export default 42;	// 指定了對外接口爲default

// 報錯
export 42;	// 沒有指定對外的接口
複製代碼

(5)ES6 模塊與 CommonJS 模塊的差別

有兩個重大差別。

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
  • ES6 中 import/export 最終都是編譯爲 CommonJS 中 require/exports 來執行的。
// CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};


// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

// lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。
// 這是由於mod.counter是一個原始類型的值,會被緩存。
複製代碼

// ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

//ES6 模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。

複製代碼

五、瞭解es6模塊的循環依賴嗎?它和CommonJs模塊的循環加載有什麼不一樣?它是如何支持模塊循環依賴的?

(1)CommonJs模塊的循環加載

CommonJS模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。CommonJS的作法是,一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

官方文檔裏面的例子以下:腳本文件a.js

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
複製代碼

上面代碼之中,a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.js執行完畢,再往下執行。

再看b.js的代碼

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
複製代碼

上面代碼之中,b.js執行到第二行,就會去加載a.js,這時,就發生了"循環加載"。系統會去a.js模塊對應對象的exports屬性取值,但是由於a.js尚未執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。

a.js已經執行的部分,只有一行。

exports.done = false;

所以,對於b.js來講,它從a.js只輸入一個變量done,值爲false

而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。咱們寫一個腳本main.js,驗證這個過程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
複製代碼

執行main.js,運行結果以下。

$ node main.js

在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
複製代碼

上面的代碼證實了兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸出緩存的b.js的執行結果,即它的第四行。

exports.done = true;

(2)es6模塊的循環依賴

ES6模塊的運行機制與CommonJS不同,它遇到模塊加載命令import時,不會去執行模塊,而是隻生成一個引用。等到真的須要用到時,再到模塊裏面去取值。

所以,ES6模塊是動態引用,不存在緩存值的問題,並且模塊裏面的變量,綁定其所在的模塊。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);


$ babel-node m2.js
bar
baz
// 代表,ES6模塊不會緩存運行結果,而是動態地去被加載的模塊取值,以及變量老是綁定其所在的模塊
複製代碼

ES6根本不會關心是否發生了"循環加載",只是生成一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。

// a.js
import {bar} from './b.js';
export function foo() {
  bar();  
  console.log('執行完畢');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}

複製代碼

按照CommonJS規範,上面的代碼是無法執行的。a先加載b,而後b又加載a,這時a尚未任何執行結果,因此輸出結果爲null,即對於b.js來講,變量foo的值等於null,後面的foo()就會報錯。

可是,ES6能夠執行上面的代碼。

$ babel-node a.js
執行完畢
複製代碼

a.js之因此可以執行,緣由就在於ES6加載的變量,都是動態引用其所在的模塊。只要引用是存在的,代碼就能執行。


引用文章:

Module 的加載實現

JavaScript 模塊的循環加載

以上

相關文章
相關標籤/搜索