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

傳送門>>>

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

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


一、你瞭解淺拷貝、深拷貝嗎?

  • 淺拷貝是隻複製一層對象的屬性,不會進行遞歸複製,而js存儲對象都是存地址的,因此淺拷貝會致使對象中的子對象指向同一塊內存地址;
  • 深拷貝則是開闢新的棧,不只將原對象的各個屬性逐一複製出去,並且會將屬性所包含的對象也依次採用淺拷貝的方式遞歸複製到新對象中,拷貝了全部層級。

淺拷貝的實現java

var obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5]
}

// 淺拷貝
// 一、經過for-in方式實現
function simpleCopy (obj) {
  if (typeof obj != 'object') {
    return false
  }
  let copyObj = {}
  for(var i in obj) {
    copyObj[i] = obj[i]
  }
  return copyObj
}
simpleCopy(obj)	// {"a":1,"b":2,"c":[3,4,5]}

// 二、經過屬性描述符
function simpleCopy2(obj) {
  if (typeof obj != 'object') {
    return
  }
  let copyObj = {}
  Object.entries(obj).forEach(item => {
    copyObj[item[0]] = item[1]
  })
  return copyObj
}
simpleCopy2(obj)

複製代碼

深拷貝的簡易版本es6

// 一、JSON
function deepCopy1 (obj) {
  return JSON.parse(JSON.stringify(obj))
}

// 二、複製屬性時,進行判斷,若是是數組或者對象,則再次調用拷貝函數
function deepCopy2(obj, copyObj) {
  if (typeof obj != 'object') {
    return
  }
  var copyObj = copyObj || {}
  for(var i in obj) {
    // 過濾null
    if (typeof obj[i] === 'object' && Object.prototype.toString.call(obj[i]) !== '[object Null]') {
      copyObj[i] = Array.isArray(obj[i]) ? [] : {}
      deepCopy2(obj[i], copyObj[i])
    } else {
      copyObj[i] = obj[i]
    }
  }
  return copyObj;
}
deepCopy2(obj)
複製代碼

推薦查看 lodash 的深拷貝函數面試

二、對閉包的理解?何時構成閉包?閉包的實現方法?閉包的優缺點?

  • 閉包:閉包就是可以讀取其餘函數內部變量的函數
  • 閉包的做用及好處:
    • 一個是前面提到的能夠讀取函數內部的變量
    • 一個就是讓這些變量的值始終保持在內存中,不會在外部函數調用後被自動清除
  • 使用閉包的注意點:
    • 因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。
    • 閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。

常見筆試題:數組

// 聲明一個函數表達式
var add = function(x){
    var sum = x;
    // 在函數表達式內部有一個求和的內部函數
    var tmp = function(y){
        sum += y;// 求和
        return tmp;
    }
    // 構建一個函數體的toString()函數
    tmp.toString = function(){
        return sum;
    }
    return tmp; // JavaScript中,打印和相加計算,會分別調用toString或valueOf函數,因此咱們重寫tmp的toString和valueOf方法,返回sum的值
}

add(1)(2)(3)(4)   // function 10
var a = add(1)(2)(3)(4).toString();  //10

// 一、函數add(1)第一次調用,實際上是隻聲明瞭var sum 這個變量,而後返回了tmp函數體,用於後面調用tmp函數

//二、函數add(1)(2)第二次調用才真正的把參數傳進來使用了,即第一次傳的 1 是沒地方用的,沒意義,第二次傳的 2 是給第一次返回的tmp函數體傳的參、即用在 sum=sum+x上 ---- sum=1+2

// 三、函數add(1)(2)(3)第三次調用和第二次同樣,因爲tmp函數體內部 return tmp 返回了自己,因此後面能夠繼續調用tmp函數,也就是除第一次調用傳參無效外,後面能夠調用無數次,sum值會不斷累加

// 四、toString是tmp函數體附帶的屬性方法函數,會隨着主體函數toString執行一次調用一次
複製代碼

三、如何理解原型?如何理解原型鏈?

  1. 每一個函數在定義時都會自動帶一個prototype屬性,該屬性是一個指針,指向一個對象,該對象稱之爲原型對象(經過prototype實現js的繼承)。閉包

  2. 原型對象上默認有一個屬性constructor,指向該原型對象對應的構造函數。app

  3. 經過調用構造函數建立的實例對象,都有一個內部屬性__proto__,指向該構造函數的原型對象。其實例對象能夠訪問該原型對象上的全部屬性和方法。函數

  4. 總結:post

    每一個構造函數都有一個原型對象,原型對象上包含一個指向構造函數的指針,而實例對象都包含一個指向原型對象的內部指針。

    通俗的說,實例對象經過內部指針__proto__訪問到原型對象,原型對象經過constructor找到構造函數。

    Foo.prototype只是一個指針,指向Foo的原型對象,利用這個指針能夠實現JS的繼承。

原型鏈的做用:

  1. 確定是爲了繼承!
  2. prototype用來實現基於原型的繼承與屬性的共享。
  3. __proto__就構成了咱們常說的原型鏈訪問構造方法中的顯示原型,一樣用於實現基於原型的繼承。
// 面試題1:
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}

var f = new F()
// 請問f有方法a 方法b嗎
f.a()	// success 
// because: f.__proto__ === F.prototype F.prototype.__proto__ === Object.prototype 

f.b()	// f.b is not a function
// 而f的原型鏈上沒通過Function.prototype

複製代碼
// 面試題2:
function Foo () {}	// 構造函數
let foo1 = new Foo()	// 實例對象
let foo2 = new Foo()	// 實例對象
let obj = {}
// 寫出 foo1 foo2 Foo Function obj Object的原型鏈 ???

foo1.__proto__ === Foo.prototype
foo2.__proto__ === Foo.prototype

Foo.__proto__ === Object.prototype
Foo.prototype === Object.prototype

Foo.prototype.__proto__ === Object.prototype
Foo.prototype.constructor === Foo

// Function.prototype是引擎創造出來的對象,一開始就有了,又由於其餘的構造函數均可以經過原型鏈找到Function.prototype,Function自己也是一個構造函數,爲了避免產生混亂,就將這兩個聯繫到一塊兒了
Function.__proto__ === Function.prototype
Function.prototype === Function.prototype

Function.prototype.__proto__ === Object.prototype
Function.prototype.constructor === Function

obj.__proto__ === Object.prototype

// Object是對象的構造函數,那麼它也是一個函數,固然它的__proto__也是指向Function.prototype
Object.__proto__ === Function.prototype	
Object.prototype === Object.prototype

Object.prototype.__proto__ === null
Object.prototype.constructor === Object

// 構造函數有一個prototype屬性,指向實例對象的原型對象
Foo.prototype === foo1.__proto__

// 經過同一個構造函數實例化的多個對象具備相同的原型對象
foo1.__proto__ === foo2.__proto__

// 原型對象上默認有一個屬性constructor,指向該原型對象對應的構造函數
Foo.prototype.constructor === Foo

foo1.__proto__.constructor === Foo

// 因爲實例對象能夠繼承原型對象的屬性,因此實例對象也擁有constructor屬性,一樣指向原型對象對應的構造函數
foo1.constructor === Foo

// isPrototypeOf用來判斷實例對象與原型對象的關係
Foo.prototype.isPrototypeOf(f1)	// true 

// Object.getPrototypeOf() 返回該實例對象對應的原型對象,和proto是同樣的,都返回原型對象
Object.getPrototypeOf(foo1)	// Foo.prototype
Object.getPrototypeOf(foo1) === foo1.__proto__

// in操做符能夠判斷某個屬性在不在對象上,但沒法區分是自有屬性仍是繼承屬性
// hasOwnPrototype能夠判斷該屬性是自有屬性仍是繼承屬性

複製代碼

四、建立對象有哪些方式?

一、對象字面量方式

var person = {
    name: 'Jack',
    age: 18,
    sayName: function () {
    	alert(this.name); 
		}
 }
// 大量重複代碼
複製代碼

二、工廠模式

function a (name, age) {
  var obj = new Object()
  obj.name = name
  obj.age = age
  obj.alert = function () {
    alert(this.name)
  }
  return obj
}
var a1 = a('name', 'age')
// 工廠模式就是批量化生產, 因爲是工廠暗箱操做的,因此你不能識別這個對象究竟是什麼類型
複製代碼

三、構造函數(用來初始化新建立的對象的函數就是構造函數)

function Person (name, age) {
  this.name = name
  this.age = age
  this.say = function () {
    alert(this.name + this.age)
  }
}
var p1 = new Person('xxx', 22)
var p1 = new Person('yyy', 18)

// 擁有相同的功能的兩個實例,卻分配了不一樣的內存,浪費了內存空間
p1.say === p2.say	// false

----------

// 構造函數拓展模式
// 把方法轉移到構造函數外部,能夠解決方法被重複建立的問題。
function Person (name, age) {
  this.name = name
  this.age = age
  this.say = say
}
function say () {
  alert(this.name + this.age)
}
var p1 = new Person('xxx', 22)
var p1 = new Person('yyy', 18)
p1.say === p2.say	// true

// 可是這是在全局做用域中定義,並且只供一個對象調用,不符合全局做用域的定義規範;而且若是有多個方法時,就要多個全局函數,嚴重污染全局空間。

-------------
// 寄生構造函數模式(是工廠模式和構造函數模式的結合)
function A (name, age) {
  var obj = new Object()
  obj.name = name
  obj.age = age
  obj.alert = function () {
    alert(this.name)
  }
  return obj
}
var a1 = new A('name', 'age')
var a2 = new A('name', 'age')

// 寄生構造函數模式與構造函數模式有相同的問題,每一個方法都要在每一個實例上從新建立一遍,建立多個完成相同任務的方法徹底沒有必要,浪費內存空間
a1.alert === a2.alert	// false

// 使用該模式返回的對象與構造函數之間沒有關係。所以,使用instanceof運算符和prototype屬性都沒有意義
a1.__proto === A.prototype	// false
複製代碼

四、原型模式

function Person () {}
Person.prototype = {
  constructor: Person,	// 顯示設置原型對象的constructor屬性
  name: 'xxx',
  age: 20,
  favoraties: [],
  say: function () {
    alert(this.name + this.age)
  }
}

var p1 = new Person()
var p2 = new Person()

p1.favoraties.push('sing')
p2.favoraties.push('song')
p1.favoraties	// ["sing", "song"]
p2.favoraties	// ["sing", "song"]

// 引用類型的值在原型對象上會被共享,修改一個實例的值,也會改變其餘實例的變化

複製代碼

五、組合使用構造模式和原型模式

function Person (name, age) {
  this.name = name
  this.age = age
  this.say = function () {
    alert(this.age)
  }
}
Person.prototype = {
  constructor: Person,
  totalHobby: 'running',
  sing: function () {
    alert(this.name)
  }
}

var p1 = new Person('xxx', 20)
var p1 = new Person('yyy', 18)

// 將獨立的屬性和方法放在構造函數中,須要共享的屬性和方法放在原型對象中,還支持向構造函數傳遞參數,這樣能夠最大限度的節省內存而又保留實例對象的獨立性

----------
// 動態原型模式
// 動態原型模式將組合模式中分開使用的構造函數和原型對象都封裝到了構造函數中,而後經過檢查方法是否被建立,來決定是否初始化原型對象,也減小了全局空間的污染

function Person (name, age) {
  this.name = name
  this.age = age
  if (typeof this.say != 'function') {
    this.prototype.say = function () {
      alert(this.age)
    }
  }
}
var p1 = new Person('name', 'age')
p1.say()	
複製代碼

五、實現繼承的多種方式和優缺點

一、原型繼承

// 原型繼承 (本質就是重寫原型對象,代之以一個新類型的實例)

function Super () {
  this.value = true
  this.colors = ['red']
}
Super.prototype.getValue = function () {
  return this.value
}
function Sub () {}
Sub.prototype = new Super()	// here
Sub.prototype.constructor = Sub

var instance = new Sub()
instance.getValue()	// true

var instance2 = new Sub()
instance.colors.push('green')

instance.colors	// ["red", "green"]
instance2.colors	// ["red", "green"]


// 第一個缺點是全部子類共享父類的實例,其中一個子類修改了父類中引用對象的值,其餘子類的屬性值也會被修改
// 第二個缺點是在構造子類實例的時候,不能給父類傳遞參數。實際上,是沒有辦法在不影響全部對象實例的狀況下,給父類傳遞參數。
複製代碼

二、構造函數實現繼承

// 構造函數繼承,也叫作 僞類繼承 或 經典繼承 (本質就是在子類構造函數內部借用call/apply方法調用父類的構造函數)

function Super (name, colors) {
  this.name = name
  this.colors = colors
}
function Sub () {
  // 繼承了Super
  Super.call(this, 'xyz', ['red'])
}
var instance1 = new Sub()
instance1.name	// xyz

instance1.colors.push('white')
instance1.colors	// ['red', 'white']

var instance2 = new Sub()
instance2.colors	// ['red']
// 相對於原型繼承來講,有一個很大的優點就是能夠傳遞參數給父類,而且也能夠解決引用類型實例屬性共享的問題。
// 缺點是方法都定義在構造函數內部,沒法複用
複製代碼

三、組合式繼承

// 組合繼承(原型繼承+構造函數繼承)

function Super (name) {
  this.name = name
  this.colors = ['red']
}
Super.prototype.getName = function () {
	return this.name
}

function Sub (name, age) {
  // 繼承屬性
  // 第二次調用父類構造函數,Sub.prototype獲得了name和colors,並覆蓋了第一次獲得的屬性
  Super.call(this, name)
  this.age = age
}
// 繼承方法
// 第一次調用父類構造函數,Sub.prototype獲得了內部屬性name,colors
Sub.prototype = new Super('xyz')
Sub.prototype.constructor = Sub
// 子類本身的方法
Sub.prototype.getAge = function () {
  return this.age
}

var instance1 = new Sub('xyz', 22)
instance1.getName()	// xyz
instance1.getAge()	// 22
instance1.colors.push('white')
instance1.colors	// ['red', 'white']

var instance2 = new Sub('zzz', 18)
instance2.colors	// ['red']

// 缺點是不管什麼狀況下,總會調用兩次父類構造函數。第一次是在建立子類原型的時候,第二次是在子類構造函數內部
// 佔用的空間更大了
複製代碼

四、寄生組合式繼承

// 寄生組合式繼承,解決了調用兩次父類構造函數的問題
// 也是借用構造函數來繼承不可共享的屬性,經過原型鏈的混成形式來繼承方法和可共享的屬性。只不過把原型繼承改爲了寄生式繼承。
// 使用寄生組合繼承能夠沒必要爲了指定子類的原型而調用父類的構造函數,因此只繼承了父類的原型屬性,而父類的實例屬性是借用構造函數的方式來獲得的。

function Super (name) {
  this.name = name
  this.colors = ['red']
}
Super.prototype.getName = function () {
  return this.name
}

function Sub (name, age) {
  Super.call(this, name)
  this.age = age
}

// 本質上,是對傳入的對象進行了一次淺複製
if (!Object.create){
  Object.create = function (proto) {
    function F() {}	// 臨時的構造函數
    F.prototype = proto	// 將傳進來的參數做爲這個構造函數的原型
    return new F()	// 返回這個構造函數的新實例
  }
}

Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub

var instance1 = new Sub('xyz', 18)
instance1.getName()	// xyz
instance1.colors.push('white')
instance1.colors	// ['red', 'white']

var instance2 = new Sub('xyz2', 22)
instance2.colors	// ['red']

// 高效率體如今只調用了一次Super構造函數,而且所以避免了在Sub.prototype上建立沒必要要的、多餘的屬性。並且原型鏈不變

複製代碼

ES5的繼承 是經過原型或構造函數機制來實現,實質上是先建立子類的實例對象,而後再將父類的方法添加到this上(Parent.apply(this))。

五、es6中的class ES6封裝了class,extends關鍵字來實現繼承,實質上是先建立父類的實例對象this(因此必須先調用父類的super()方法),而後再用子類的構造函數修改this。

// class

class Super {
  constructor (name) {
    this.name = name
    this.colors = ['red']
  }
	
  getName () {
    return this.name
  }
}

class Sub extends Super {
  constructor (name, age) {
    super(name)
    this.age = age
  }
  getAge () {
    return this.age
  }
}

var instance1 = new Sub('xyz', 18)
instance1.colors.push('white')
instance1.colors	// ['red', 'white']
instance1.getName()	// xyz

var instance2 = new Sub('xyz2', 22)
instance2.colors	// ['red']
instance2.getAge()	// 22

複製代碼
相關文章
相關標籤/搜索