字節跳動面試官:啥特麼都有我

前言

面向對象相信對你們來講確定不陌生,基本上現代web開發不會這玩意以及無法寫東西了javascript

可是呢,用歸用,相信你們日常工做中須要寫原生對象的地方並無多少,本篇文章來對面向對象進行一個總結和梳理html

概念

我們先來講一下概念,由於不一樣人叫面向對象名字也不一樣,這裏屬性變量意思都是同樣的,我們不去糾結他們具體叫什麼vue

對象組成能夠分爲兩大類:java

  1. 屬性、變量、狀態、數據
  2. 過程、方法、函數

  • 類(class): 類自己,通常沒什麼功能,除非是Math這種基本全是靜態方法的類
  • 實例(instance):類被實例化以後的東西,就叫實例
  • 成員(meber):包括(屬性+方法)
    • 實例成員(實例上才能用的屬性和方法,相似str.length不能直接寫成String.length
    • 類成員(相似Math.xxx),無需實例化就能夠用

打個比方let oDate = new Date() 這個Date就是類,而這個oDate 就是實例node

而絕大部分人再說對象的時候都是在說實例react


抽象

抽象這個詞相信你們已經快聽吐了,可是又不是很好理解,這裏大概說一下ios

  1. 提取過程——設計類、設計程序
    • 這個其實很好理解,在寫一個程序的時候,好比咱們要作一個聊天的功能,整理需求,而後定義好本身須要的類,好比這個用戶須要有名字,和頭像等等這個過程,就叫抽象
  2. 抽象類
    • 這個可能稍微複雜一點,有那麼一種類,是不實現任何功能的
    • 可能有人看到這就有點不理解,爲啥我要寫一個不實現任何功能的累呢,這裏爲了方便用react 舉個例子
    • 我們都知道react中寫組件須要class App extnds React.Component... 而這裏面必需要有一個render方法,若是沒有,它就會給你報錯
    • 固然了,這裏主要爲了說明例子,不包括hook什麼的,別槓我,我懼怕
    • 而抽象類能夠實現這個功能,給全部的類加一個公共的基類(也就是父類)統一處理
    • react中的這個Component就是實現了一個抽象類

面向對象思想

這個詞兒也是被用爛了,你們無論是從別的文章看過也好,仍是找過什麼資料,看了一大堆,仍是不明白啥叫面向對象思想es6

封裝

封裝的最基本的目的是——爲了保護我們或者實例當中的各類成員web

事實上來講我們無論寫什麼面向對象也好,面向過程也好,面向切片等等等等,最終的目的就是爲了讓這個程序出錯少,效率高,易於調試和拓展面試

而接下來我們就能想到,程序都是人寫的,人確定就會犯錯,容易偷懶或者僥倖心理

而我們封裝的類,每每會有不少的數據須要內部處理,好比有一個請求隊列的列表,你在你內部聲明好了,而後若是想取消須要用戶直接調你的方法,可是若是不保護的化,可能有人犯懶直接就看到這是個數組直接就操做了,這時候若是出錯了或者這個類有其餘的地方沒作相應的操做就很難查找

因此這個封裝大概有四重目的

  1. 保護成員
  2. 數據隱藏——方法
  3. 強制訪問權限
  4. 便於理解

繼承

任何一個類,能夠不用從零開始寫,能夠在一個原有的類的基礎之上作一些修改等等這就叫繼承

  1. 重用代碼
  2. 無需修改父類
  3. 能夠抽象類

這個繼承相信你們仍是沒問題的,這裏很少贅述

多態

多態也屬於一種抽象,也是一種比較經常使用的思想,好比如今公司有一個轉帳的功能,支持多個國家互相轉的,那這個錢不同匯率也就不同,以及其餘的一些問題

固然這時候直接用if判斷確定也是能夠的,可是這不是感受不夠裝* 麼,這時候就能夠用多態,把這個錢統一處理,至於具體匯率什麼的細節,分別交給他們本身來處理,這個思想,就是多態

總結

相信看到這你們應該已經明白大概的意思了,不過對於初學者來講仍是不太明白到底具體到項目能怎麼寫,別慌,一步一步來

有一點須要確認,你們可千萬別覺得面向對象是語言所獨有的東西,好比java是面向對象的,c是面向過程的,其實也不是,面向對象思想幾乎在全部的地方都有用

好比說數據庫,也有面向對象數據庫——ORM,它裏面存的就不是像excel那樣的數據了,而是一個對象,有相應匹配的操做等等一系列的

寫法

首先我們要知道,任何一種類,都須要構造函數,什麼叫構造函數呢,很簡單,就是當你這個類實例化的時候,須要作一些初始化的工做

es6以前

es6以前,類和構造函數是不分的,這也是很很差的一點,如今若是想實現一個類,直接拽一個function 在這很難分辨就是是一個函數仍是,因此,這個函數既是構造函數也是類

我們來直接寫一個看看

function A() {
  console.log('hello');
}

var a = new A();
複製代碼

能夠看到,在我們實例化的過程中,就會運行函數內的代碼

以及,我們若是相加屬性和方法也是很簡單的

屬性和es6版本同樣,直接加this就行

方法放在prototype上,這個沒什麼可說的

繼承

es6以前沒給我們提供直接繼承的方法,因此得我們手動操做,因此我們得明白繼承是幹什麼 繼承就是把父類的方法+屬性拿過來就能夠了

首先,我們先來隨便寫一個類出來,像這樣

而後搞一個子類,而且新增一個參數,而後我們把參數先拿過來,怎麼拿呢? 如今我們的類是一個函數,是函數就能執行,我們只須要把A拿過來執行一下而且call到本身身上,再把A須要的參數傳過去,這樣就能拿到全部的參數了,像這樣

好的,屬性我們如今已經能夠拿過來了,那麼方法怎麼拿過來了,方法在哪,在Aprototype上,那能直接B.prototype = A.prototype麼?首先確定一點,這麼寫,東西確定能出來,不過問題你們也知道,js裏存在引用,給子類添加方法父類也被修改了這確定不行

不囉嗦了

我們能夠直接B.prototype = new A()

我們能夠看到,東西是能出來的,父類的方法也沒有被污染到,這是爲何呢,這裏就不得不搞出來原型鏈這個概念了

有大白話講就是 找一個實例要東西,它會先從本身實例身上找,找不到的話再找本身的prototype,可是我們如今這個prototype指向的就是A的實例,因此從B實例找不到後去找prototype的時候,找不到就回去找A的實例,A的實例找不到就回去找Aprototype 而後就能夠了

好玩不

相信看到這,你們應該已經明白es6以前的寫法缺陷了,功能確定是都能實現,可是太亂了,一個團隊好幾十人,你搞你的,我搞個人就亂套了

es6

我們es6class就簡單多了,直接提供了一個關鍵字class,直接寫就好了,不過本質上來講只是語法糖而已,因此建議你們仍是看看es6以前的寫法

寫法也是很簡單的,注意一點,這裏面的方法並非函數的簡寫,寫一個function的話,反而會報錯

繼承

繼承es6也有專門的關鍵字來講明,就叫extends

很簡單對不對?

注意⚠️ 在子類什麼都不寫的狀況下,默認會給你加一個constructor,若是你本身寫了constructor,就至關於你須要本身來了

主要要乾的就是須要把父類的屬性拿過來,es6有一個super關鍵字,就至關於直接把父類constructor直接在子類裏執行了一遍,不用像以前同樣要麼apply,要麼call的,很亂

注意⚠️ 在我們構建子類的時候,須要完成父類的構建,也就是那個super,若是先用thissuper的時候會報錯


this

this相信你們確定常常用,而且一會變成這個,一會變成那個,特別的亂 this取決於誰在調用,this就是誰 而this是用於函數/方法內部,而誰在調用這個函數,this就指向誰

js自己又是全局的東西都屬於window,因此我們這麼寫

function fn() {
    console.log(this);
}

fn();
複製代碼

就徹底等價於這麼寫

function fn() {
    console.log(this);
}

window.fn();
複製代碼

因此console出來的this也是window

我們可能看過不少面試題都有相似這樣的題目

總的來講就是一個函數給這個對象那個對象的,而後打印this.xxx,其實想明白這件事,一切都會變得很簡單了

如今這個arr.fn = fn了,而調用的時候,是arr在調用,因此這個this就是arr

固然了,事件也是同樣的

很簡單對不對?

順便一說,js的this這麼亂是由於做者自己想讓它變得更簡單,誰調就是誰,多好呀~ 不過這每每在寫一些大項目的時候會有一堆問題

嚴格模式

js做者後來又出了一個嚴格模式,由於全局的東西都屬於window這事兒原本也不靠譜,畢竟js運行場景已經不少了,好比nodejs,就沒有window的概念

而用嚴格模式也很簡單,直接在script里加一個"use strict" 就能夠,像這樣

這時候console出來的就是undefined

定時器

不過這個this還收一點影響,也就是定時器

能夠看到我如今是開着嚴格模式的狀況下,可是console的仍是window

不過這卻是也好解釋,畢竟這個定時器window的,是瀏覽器調的,也是經過window間接的來執行到了這個函數


操做this

js中有兩種操做this的方法

  1. 函數的方法

    • 我們都知道直接寫一個函數也就至關於new了一個Function類,因此這麼寫
    function fn() {
        alert(this);
    }
    複製代碼

    徹底等價於

    var fn = new Function('alert(this)');
    複製代碼
    • 因此函數也是個對象,既然是對象,那麼也就有方法
    1. call

      • 我們正常運行函數多是直接fn();,用了這個直接在括號前加一個call就能夠,像這樣
      • 這個你傳什麼,它的this就是什麼,隨便傳,想這個例子,consolethis就是"aaa"
      • 至於參數,直接日後堆就能夠了
    2. apply

      • 這個apply就很簡單了,和call基本一摸同樣,區別在於call跟參數是直接堆在後面,apply的其餘參數是放在一個數組裏的,像這樣
    3. bind

      • 這個bind跟前二者不同,callapply都是直接運行了,bind的做用是返回一個新函數
      • 能夠看到,無論生成的新函數在哪運行,誰調用this都是不變的
  2. 箭頭函數

    • 箭頭函數內部的this永遠跟剪頭函數外部的this一致,也就是上下文
    • 正常狀況下這個document.onclick裏的函數的this指向HTMLDocument,因此找不到aaa,會consoleundefined,這個很好理解
    • 改爲箭頭函數以後,這回就對了,由於它的上下文環境在class A裏,這就沒問題了
    • 固然了,用bind也是能夠的,這不是方便麼

類型檢測

typeof

typeof更適合檢測基本類型:number、boolean、string、function、Object、undefined

這個相信你們也都用過,也就很少說了,檢測基本類型表明我們分不清一個對象究竟是數組仍是json或者是map等等 全部對象全都是object,那這個確定知足不了需求

instanceof

這個instanceof是能夠檢測具體類型的,並且不光能夠檢測子類,對子類的父類也能檢測到

其實這不能算是問題,由於正常來講子類原本就>=父類

constructor

可是有時候咱們就是不想對父類也有反應,就是想檢測是否是屬於我這個類,這是有直接用constructor來判斷就很方便 constructor是能夠用於精確匹配的

這個constructor極少的狀況會用到,自己並非用來判斷類型的,而是返回實例的構造器,不過正好我們能夠間接作一個類型判斷而已 正常狀況用typeofinstanceof就已經足夠了

固然了,可能有人看到es6以前的class寫法已經要噴我了,由於那麼寫用constructor判斷又有問題,由於那麼寫子類constructor就會變成父類了,因此還須要重置一下

等等等等把,es6以前的寫法等等須要處理細節方面還不少,由於如今實在是不經常使用,因此本文就很少贅述了


高階類-HOC

高階類這個詞可能有人沒聽過,這裏簡單描述一下
通常狀況下,都是子類繼承父類,而後子類可使用父類當中的東西,而這個高階類,能夠反過來,父類使用子類的東西

順便一說,不是隻有js纔有高階類的概念,幾乎全部語言都有,只是方不方便的問題

我們直接來寫一個

能夠看到,直接在class A 上,並無bbb這個東西,而直接子類去繼承的時候,子類身上有,間接着就能夠調用了

這就是所謂的高階類,賊簡單吧

不過直接這麼說你們可能感受不到有什麼應用場景,其實也不太經常使用,在寫一些工具或者框架的時候可能會用到,這裏簡單寫個小例子,多個類之間如何用高階類共享數據 固然了,高階類的用途有不少,這只是其中一種而已

class Store {
    constructor() {
      this.state = {};
    }
    get(key) {
      return this.state[key];
    }
    set(key, value) {
      this.state[key] = value;
    }
    conect(cls) {
      let _this = this;
      return new (class extends cls {
        constructor(...args) {
          super(args);
          this.get = _this.get.bind(_this);
          this.set = _this.set.bind(_this);
        }
      })();
    }
  }

  let store = new Store();

  let a = store.conect(class {});

  a.set('aaa', '我是aaa');

  let b = store.conect(class {});

  console.log(b.get('aaa'));
複製代碼

這個例子很簡單,主要就是要分清誰是父類,我們把數據全存到store類裏,而後接受一個類參數,內部繼承一個新的類,而後添加出新的方法,而在使用者來講,也就是a類和b類,這些都不用管,他們本身就是父類,來調用子類的方法

是否是很簡單?

大概就是這種感受,至於工做當中怎麼用,就看你們本身的業務場景了

可相應對象

所謂的可相應對象就是你操做這個對象的時候能夠收到通知,好比vue,你們應該都知道vue頁面,就算直接在瀏覽器f12控制檯中,直接vm.xxx=xxx對某一項數據修改,頁面中就會發生變化,這就是可相應對象

訪問器

這個訪問器就是在原型的方法前加get或者set,至於內部要幹什麼,你本身決定,像這樣

用過ts 版的vue2.x的你們都知道,computed 就是用這玩意的原生語法

訪問器仍是比較簡單的,在一些小的場景中用的比較多

defineProperty

defineProperty接受三個參數(data,key, option)

這個data就是你的數據源,這個key就是你要監聽哪一個屬性,剩下的具體的操做放在option裏,好比getset等等,我們直接來試試

var json = {
    _num: 0,
};
Object.defineProperty(json, 'num', {
get() {
  console.log('get');
  return this._num;
},
set(val) {
  console.log('set');
  this._num = val;
  return this._num;
},
});

console.log(json.num);
json.num++;
console.log(json.num);
複製代碼

是否是很簡單,固然了,細節上還有不少,好比delete,我們知道正常狀況下js中的json是能夠直接delete某個key的,可是我們如今直接這麼寫是不行的

我們能夠給一個參數configurable: true,默認的狀況是false

更多參數介紹你們能夠看MDN文檔中的defineProperty,這裏就很少贅述

我們這裏用的只是json,你們可能會感受跟訪問器區別不大,並且,平時我們也不是直接對json用,而是像vue 2.x同樣又能操做實例,又能this直接訪問或修改屬性

類中的可相應對象

我們都知道 vue2.x中能夠這麼修改屬性

知道了這點以後我們就能夠幹不少事兒了,我們能夠直接監聽這個來進行return

這樣我們就能夠監聽到變化了,能監聽到變化了天然就能夠緊接着幹其餘的事了,好比從新渲染頁面等等

固然了,真正Vue中可徹底不是這麼寫的,由於它東西不少,並且全部數據都須要監聽,我們如今只是單單監聽一個對象裏的key,方法我們也知道,循環+遞歸麼,不過要把這些東西整理出來其實也是很複雜的,好比頁面屬性怎麼跟你的data綁定,組件間的傳值,渲染、虛擬dom等等

等等吧,若是真拓展成一個方便的框架或者小工具仍是有不少工做的, 這裏只是說明方法,有什麼更方便的用法和場景還須要你們本身鑽研

defineProperty缺陷

用過vue的你們都知道vue中是不能直接用下標修改數組中的某一條數據的,vue做者也推出了$set解決這個問題,這個就是defineProperty的問題

  1. 對數組下標直接賦值
  2. json對象沒有的key賦值

你們能夠看到其實還好,雖然問題不大,不過沒有確定比有強,proxy沒這個問題,爲啥不直接用proxy

proxy

defineProperty有點不一樣,defineProperty是操做監聽的原始對象,而proxy是操做返回出來的新對象

有兩個參數,一個是數據,一個是對應的操做,這裏面有幾個經常使用的參數

  • has 這個對應的就是 js 中的in操做
    • 我們知道js裏能夠直接"a" in {a: 12}這麼判斷
    • 而這個就會觸發has,像這樣
    • 這個data就是原始數據,基本上每一個方法裏都會把原始數據給你,很方便
  • get 獲取,這個相信不用多說,都差很少
  • set 設置
  • deleteProperty
    • 這個其實就是delete,只不過delete是關鍵字,因此取了個這個名字
  • apply 這個apply其實頗有用,它能夠監聽一個函數,在編寫一個axios這樣的庫 我也用到了,有興趣你們能夠看一下
    • apply有三個參數,第一個仍是data,可是因爲我們是監聽函數,因此就是那個函數自己,第二個就是誰在調用的那個this,上文講過誰調用函數this就指向誰,因此這個第二個參數就是那個this,第三個參數就是我們運行這個函數的時候傳進來的參數
  • construct 監聽類
    • 它有兩個參數,一個是監聽的那個class,一個是參數,其實用起來感受跟高階類有點類似
    • 不過通常用起來,我們還須要再配合一個proxy,由於要監聽我們通常都是要監聽這個類上的屬性有沒有改變之類的,因此還要單獨再監聽一遍實例返回出去

剩下的參數你們感興趣的話也能夠去MDN中的Proxy去了解,也很少贅述

完整的監聽例子

看到這你們應該都對js中的可監聽對象瞭解的差很少了,這裏附上一個相對完成一點的監聽例子

相似vue中的datadata能夠是值,也能夠是json,也能夠是數組,數組裏套jsonjson套數組等等,我們能夠分別判斷一下而後套一個遞歸就搞定了

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <script> function createProxy(data, cb) { let res; if (data instanceof Array) { res = []; for (let i = 0; i < data.length; i++) { if (typeof data[i] == 'object') { res[i] = createProxy(data[i], cb); } else { res[i] = data[i]; } } } else { res = {}; for (let key in data) { if (typeof data[key] == 'object') { res[key] = createProxy(data[key], cb); } else { res[key] = data[key]; } } } return new Proxy(res, { get(data, name) { return data[name]; }, set(data, name, val) { data[name] = val; cb(name); return true; }, }); } let _json = { arr: [ { a: [1, 2, 3], }, 321, ], json: { aaa: { bbb: { ccc: 111, }, }, }, name: 'name', }; var p = createProxy(_json, function(name) { console.log('set'); }); </script>
  </body>
</html>

複製代碼

篇幅較長

感謝觀看

若是有問題的話能夠直接在評論區留言或者加我微信一塊兒溝通

相關文章
相關標籤/搜索