JS 簡潔代碼編寫與技巧指北

if 層級嵌套優化

============

如下面的三層 if 條件嵌套

function froutCheck(fruit, quantity) {
  const redFruits = ['apple''pear''cherry''banana'];
  // 條件 1: 水果存在
  if(fruit) {
    // 條件 2: 屬於紅色水果
    if(redFruits.includes(fruit)) {
      console.log('紅色水果');
      // 條件 3: 水果數量大於 10 個
      if (quantity > 10) {
        console.log('數量大於 10 個');
      }
    }
  } else {
    throw new Error('沒有水果啦!');
  }
}

“早處理早返回” 的原則來寫的話,邏輯會更加清晰和容易維護

function supply(fruit, quantity) {
  const redFruits = ['apple''pear''cherry''banana'];
  if(!fruit) throw new Error('沒有水果啦');     // 條件 1: 當 fruit 無效時,提前處理錯誤
  if(!redFruits.includes(fruit)) return; // 條件 2: 當不是紅色水果時,提前 return
    
  console.log('紅色水果');
    
  // 條件 3: 水果數量大於 10 個
  if (quantity > 10) {
    console.log('數量大於 10 個');
  }
}

Array 的妙用

Array.includes 判斷多個 if 條件

可以在數組中存儲多個值,使用數組 include 方法。

//Longhand
if (x === 'abc' || x === 'def' || x === 'ghi' || x ==='jkl') {
  //logic
}

//Shorthand
if (['abc''def''ghi''jkl'].includes(x)) {
  //logic
}

Array.find 查找符合條件的數組元素

當我們確實有一個對象數組並且我們想要根據對象屬性查找特定對象時,find 方法確實很有用。

const data = [
  {
    type: 'test1',
    name: 'abc'
  },
  {
    type: 'test2',
    name: 'cde'
  },
  {
    type: 'test1',
    name: 'fgh'
  },
]
function findtest1(name) {
  for (let i = 0; i < data.length; ++i) {
    if (data[i].type === 'test1' && data[i].name === name) {
      return data[i];
    }
  }
}

//Shorthand
filteredData = data.find(data => data.type === 'test1' && data.name === 'fgh');
console.log(filteredData); // { type: 'test1', name: 'fgh' }

數組中所有項都滿足某條件:Array.every

數組中是否有某一項滿足條件:Array.some

還有 Array.find、Array.slice、Array.findIndex、Array.reduce、Array.splice 等,在實際場景中可以根據需要使用。

真值判斷簡寫

// Longhand
if (tmp === true) or if (tmp !== "") or if (tmp !== null)

// Shorthand 
//it will check empty string,null and undefined too
if (test1)

注意:該方式主要用於 nullundefined 的檢查,** 特別需要注意 tmp=0 或者 tmp=‘0’** 都是 false。

多條件的 && 運算符

如果僅在變量爲 true 的情況下才調用函數,則可以使用 && 運算符。

//Longhand
if (test1) {
 callMethod();
}

//Shorthand
test1 && callMethod();

三元運算符實現短函數調用

我們可以使用三元運算符來實現函數的直接執行。

// Longhand
function test1() {
  console.log('test1');
};
function test2() {
  console.log('test2');
};
var test3 = 1;
if (test3 == 1) {
  test1();
} else {
  test2();
}

// Shorthand
(test3 === 1? test1:test2)();

對象屬性解構簡寫

let test1 = 'a';
let test2 = 'b';

//Longhand
let obj = {test1: test1, test2: test2};

//Shorthand
let obj = {test1, test2};

比較大小使用 a - b > 0 的方式更好,用 a > b 有時候會出現錯誤

錯誤用法
'20' > '100'  // true

預期結果
'20' - '100' > 0   // false

//數組排序算法
arr.sort((a, b ) =>{
  return a-b 
})

If,for…in,for…of 和的使用

  1. 能用三元運算符就用, 減少 if 的嵌套

  2. 減少多餘條件判斷,查詢結束立即返回 (早返回,多返回), 如果是函數返回 if 裏面和外面返回相同的數據類型

3.If…else if…else 多個條件時以 else 結尾, 因爲符合防禦性編程規則

4.NaN 不應該用於比較, 應該是判斷是否是數字

5.Switch…case 使用在至少有三個判斷值, case 不可省, 每次 case 必須用 break 跳出

6.for…of 遍歷數組和字符串

7.for…in 遍歷對象

For…in 遍歷對象包括所有繼承的屬性, 所以如果只是想使用對象本身的屬性需要做一個判斷

  1. 在循環內部聲明函數慎用, 因爲是循環執行完成函數調用纔會執行

9.Return 後面不要寫代碼

if 多條件判斷

“早點返回,經常返回 “(return early,return often),If 語句用來處理主路徑上的異常

比如下面的 find 函數。

function find(predicate, arr) {
    for (let item of arr) {
        if (predicate(item)) {
            return item;
        }
    }
}

在這個 find 函數內,一旦我們找到想查找的對象就立刻返回該對象並且退出循環。這使得我們的代碼更加高效。

switch 語句優化

低級版:太容易忘記寫 break

let notificationPtrn;
switch (notification.type) {
    case 'citation':
        notificationPtrn = 'You received a citation from {{actingUser}}.';
        break;
    case 'follow':
        notificationPtrn = '{{actingUser}} started following your work';
        break;
    case 'mention':
        notificationPtrn = '{{actingUser}} mentioned you in a post.';
        break;
    default:
        // Well, this should never happen
}

優化一:用 return

function getnotificationPtrn(n) {
        switch (n.type) {
            case 'citation':
                return 'You received a citation from {{actingUser}}.';
            case 'follow':
                return '{{actingUser}} started following your work';
            case 'mention':
                return '{{actingUser}} mentioned you in a post.';
            default:
                throw new Error('You’ve received some sort of notification we don’t know about.';
        }
    }
    let notificationPtrn = getNotificationPtrn(notification);

優化二:採用對象字典

function getNotificationPtrn(n) {
    const textOptions = {
        citation: 'You received a citation from {{actingUser}}.',
        follow:   '{{actingUser}} started following your work',
        mention:  '{{actingUser}} mentioned you in a post.',
    }
    return textOptions[n.type];
}

優化三(推薦) 類似模式匹配方法

const textOptions = {
    citation: 'You received a citation from {{actingUser}}.',
    follow:   '{{actingUser}} started following your work',
    mention:  '{{actingUser}} mentioned you in a post.',
}
function getNotificationPtrn(textOptions, n) {
    return textOptions[n.type];
}
const notificationPtrn = getNotificationPtrn(textOptions, notification);

若包含未知處理,我們也可以把默認的信息作爲參數

constdefaultTxt= 'You’ve received some sort of notification we don’t know about.';   // 默認值
function getNotificationPtrn(defaultTxt, textOptions, n) {
    return textOptions[n.type] || defaultTxt;
}
const notificationPtrn = getNotificationPtrn(defaultTxt, textOptions, notification.type);

現在 textOptions 是一個變量。並且該變量不用再被硬編碼了。我們可以把它移動到一個 JSON 配置文件中,或者從服務器獲取該變量。現在我們想怎麼改 textOptions 就怎麼改。我們可以增加新的選項,或者移除選項。我們也可以把多個地方不同的選項合併到一起。

鏈式取值 a[0].b 不存在的 undefined 問題

開發中,鏈式取值是非常正常的操作,如:

res.data.goods.list[0].price

但是對於這種操作報出類似於 Uncaught TypeError: Cannot read property 'goods' of undefined 這種錯誤也是再正常不過了,如果說是 res 數據是自己定義,那麼可控性會大一些,但是如果這些數據來自於不同端(如前後端),那麼這種數據對於我們來說我們都是不可控的,因此爲了保證程序能夠正常運行下去,我們需要對此校驗:

if (res.data.goods.list[0] && res.data.goods.list[0].price) {
// your code
}
如果再精細一點,對於所有都進行校驗的話,就會像這樣:
if (res && res.data && res.data.goods && res.data.goods.list && res.data.goods.list[0] && res.data.goods.list[0].price){
// your code
}

不敢想象,如果數據的層級再深一點會怎樣,這種實現實在是非常不優雅,那麼如果優雅地來實現鏈式取值呢?

方法一. 通過函數解析字符串(lodash 的 _.get 方法)

我們可以通過函數解析字符串來解決這個問題,這種實現就是 lodash 的 _.get 方法 (www.lodashjs.com/docs/4.17.5…)

// _.get(object, path, [defaultValue])  隨後一個參數是取值爲undefined時候的默認值
var object = { a: [{ b: { c: 3 } }] };
var result = _.get(object, 'a[0].b.c', 1);
console.log(result);
// output: 3
該方法源碼實現起來也非常簡單,只是簡單的字符串解析而已:
function get (obj, props, def) {
    if((obj == null) || obj == null || typeof props !== 'string') return def;
    const temp = props.split('.');
    const fieldArr = [].concat(temp);
    temp.forEach((e, i) ={
        if(/^(\w+)\[(\w+)\]$/.test(e)) {
            const matchs = e.match(/^(\w+)\[(\w+)\]$/);
            const field1 = matchs[1];
            const field2 = matchs[2];
            const index = fieldArr.indexOf(e);
            fieldArr.splice(index, 1, field1, field2);
        }
    })
    return fieldArr.reduce((pre, cur) ={
        const target = pre[cur] || def;
        if(target instanceof Array) {
            return [].concat(target);
        }
        if(target instanceof Object) {
            return Object.assign({}, target)
        }
        return target;
    }, obj)
}
// 使用
var c = {
     a: {
          b : [1,2,3] 
     }
}
_get(c ,'a.b')     // [1,2,3]
_get(c, 'a.b[1]')  // 2
_get(c, 'a.d', 12)  // 12

其實類似的問題如果用 typescript 編寫會在靜態檢查的時候就處理掉了,可以避免此類低級錯誤

方法二,使用解構賦值

這個思路是來自 github 上 You-Dont-Need-Lodash-Underscore 這個倉庫,看到這個的時候真的佩服

缺點是:可讀性不太好

const obj = {
    a:{
        b: [1,2,3,4]
    },
    a1: 121,
    a2: 'name'
}
let {a: result} = obj     // result : {b: [1,2,3,4]}
let {a1: result} = obj   // result: 121
let {b: result} = obj     // result: undefined
當然,這個時候爲了保證不報undefined,我們仍然需要定義默認值, 
let {b: result = 'default'} = obj    // result: 'default'

方法三,使用 Proxy

function pointer(obj, path = []) {
    return new Proxy(() ={}{
        get (target, property) {
            return pointer(obj, path.concat(property))
        },
        apply (target, self, args) {
            let val = obj;
            let parent;
            for(let i = 0; i < path.length; i++) {
                if(val === null || val === undefined) break;
                parent = val;
                val = val[path[i]]    
            }
            if(val === null || val === undefined) {
                val = args[0]
            }
            return val;
        }
    })
}
使用方法:
var c = {
    a: {
        b : [1,2,3] 
    }
}
pointer(c).a();   // {b: [1,2,3]}
pointer(c).a.b(); // [1,2,3]
pointer(d).a.b.d('default value');  // default value

爲啥 new Proxy() 第一個參數要是一個箭頭函數,我看大部分都是一個對象

類型強制轉換

###string 強制轉換爲數字

可以用 * 1(乘以 1)來轉化爲數字 (實際上是調用. valueOf 方法) 然後使用 Number.isNaN 來判斷是否爲 NaN,或者使用 a !== a 來判斷是否爲 NaN,因爲 NaN !== NaN

'32' * 1            // 32
'ds' * 1            // NaN
null * 1            // 0
undefined * 1    // NaN
1  * { valueOf: ()=>'3' }        // 3

常用:也可以使用 + 來轉化字符串爲數字

'123'            // 123'ds'               // NaN
+ ''                    // 0
+ null              // 0
+ undefined    // NaN
+ { valueOf: ()=>'3' }    // 3

“+ -” 符號的特技

用 + 可以將數值型的字符串轉爲數字,但是必須結合 isNaN(), isNaN() 函數用於檢查其參數是否是非數字,不是數字則爲 true

基礎知識

let a = "2" ,b= '20',c=34 ,d='2'

a-b = -18

c-d = 32

a+b = "220"

a+c = "234"

isNaN('ad')   // true

isNaN('22')   // false

騷操作

+('ab')  // NaN

+('22')  // 22

+(22)    // 22

let a = 1 , b ='2'

let c = a + +(b)    //3

示例

// 選擇最小值的函數如下(源於《vuejs權威指南》裏看到表單驗證validator.js源碼)

export default  min (val, areg){

  return !isNaN(val) && !isNaN(areg) && +(val)>= +(areg)

}

使用 filter 過濾數組中的所有假值

我們知道 JS 中有一些假值:false,null,0,"",undefined,NaN,怎樣把數組中的假值快速過濾呢,可以使用 Boolean 構造函數來進行一次轉換

const compact = arr => arr.filter(Boolean)
compact([0, 1, false, 2, '', 3, 'a''e' * 23, NaN, 's', 34])             // [ 1, 2, 3, 'a''s', 34 ]

小符號特技
-----

### 雙位運算符 ~~ 實現向下取整

可以使用雙位操作符來替代 Math.floor( )。雙否定位操作符的優勢在於它執行相同的操作運行速度更快。

_math.floor_(x) 返回小於參數 x 的最大整數, 即對浮點數向下取整

Math.floor(4.9) === 4      //true // 簡寫爲: ~~4.9 === 4      //true

不過要注意,對負數來說 ~~ 運算結果與 Math.floor( ) 運算結果不相同:

~~4.5            // 4 Math.floor(4.5)        // 4 ~~-4.5        // -4 Math.floor(-4.5)        // -5

短路運算符 &&、||

|| 分配默認值

let test1 = null,
    test2 = test1 || '6';

console.log(test2); // 輸出爲 "6"

我們知道邏輯與 && 與邏輯或 || 是短路運算符,短路運算符就是從左到右的運算中前者滿足要求,就不再執行後者了;可以理解爲:

let param1 = expr1 && expr2
let param2 = expr1 || expr2

| 運 算 符 | 示例 | 說明 |
| --- | --- | --- |
| && | expr1&&expr2 | 如果 expr1 能轉換成 false 則返回 expr1, 否則返回 expr2. 因此, 在 Boolean 環境中使用時, 兩個操作結果都爲 true 時返回 true, 否則返回 false. |
|   
 | | | expr1 |
| ! | !expr | 如果單個表達式能轉換爲 true 的話返回 false, 否則返回 true. |

因此可以用來做很多有意思的事,比如給變量賦初值:

let variable1 let variable2 = variable1  || 'foo'


如果 variable1 是真值就直接返回了,後面短路就不會被返回了,如果爲假值,則會返回後面的 foo。

也可以用來進行簡單的判斷,取代冗長的 if 語句:

let variable = param && param.prop


如果 param 如果爲真值則返回 param.prop 屬性,否則返回 param 這個假值,這樣在某些地方防止 param 爲 undefined 的時候還取其屬性造成報錯。

**但是要注意以下反例如果在數據爲 0(或者空字符串的時候也要返回數據),此時用 || 運算符就會搞錯了,返回的是默認值了:**

// 返回值爲 let res = {      data: 0,     // 後臺返回狀態碼(0表示不存在,1表示存在)      code: 200 } let val = res.data || '無數據' console.log(val)       // '無數據',其實我們要的是data的值


### 取整 | 0

對一個數字 | 0 可以取整,負數也同樣適用,num | 0

1.3 | 0         // 1 -1.9 | 0        // -1

判斷奇偶數 & 1

對一個數字 & 1 可以判斷奇偶數,負數也同樣適用,num & 1

const num=3;
!!(num & 1)                    // true
!!(num % 2)                    // true

函數
--

### 函數默認值

func = (x, m = 3, n = 4 ) => (x * m * n); func(2)             //output: 24 func(null)             //output: 12   因爲1*null = 0


注意,傳入參數爲 undefined 或者不傳入的時候會使用默認參數,即使**傳入 null 還是會覆蓋默認參數**。

這一點和 x = params || '2'是有區別的,|| 的取值時間是 params == false### 強制參數, 缺失報錯

默認情況下,如果不向函數參數傳值,那麼 JS 會將函數參數設置爲 undefined。其它一些語言則會發出警告或錯誤。要執行參數分配,可以使用 if 語句拋出未定義的錯誤,或者可以利用強制參數。

logError = ( ) => {   throw new Error('Missing parameter!'); } foo = (bar = logError( ) ) => {     // 這裏如果不傳入參數,就會執行logError 函數報出錯誤(此處也可以添加日誌打印等操作)   return bar; }


### 箭頭函數隱式返回值

返回值是我們通常用來返回函數最終結果的關鍵字。只有一個語句的箭頭函數,可以隱式返回結果(函數必須省略大括號 { },以便省略返回關鍵字)。

要返回多行語句(例如對象文本),需要使用 () 而不是 {} 來包裹函數體。這樣可以確保代碼以單個語句的形式進行求值。

function calcCircumference(diameter) {   return Math.PI * diameter } // 簡寫爲: calcCircumference = diameter => (   Math.PI * diameter; )


### 惰性載入函數

在某個場景下我們的函數中有判斷語句,這個判斷依據在整個項目運行期間一般不會變化,所以判斷分支在整個項目運行期間只會運行某個特定分支,那麼就可以考慮惰性載入函數

// 低級版

function foo(){     if(a !== b){         console.log('aaa')     }else{         console.log('bbb')     } }   // 優化後 function foo(){     if(a != b){         foo = function(){             console.log('aaa')         }     }else{         foo = function(){             console.log('bbb')         }     }     return foo(); }


那麼第一次運行之後就會**覆寫**這個方法,下一次再運行的時候就不會執行判斷了。當然現在只有一個判斷,如果判斷很多,分支比較複雜,那麼節約的資源還是可觀的。

### 一次性函數

跟上面的惰性載入函數同理,可以在函數體裏覆寫當前函數,那麼可以創建一個一次性的函數,重新賦值之前的代碼相當於只運行了一次,適用於運行一些只需要執行一次的初始化代碼

var sca = function() {     console.log('msg')     sca = function() {         console.log('foo')     } } sca()        // msg sca()        // foo sca()        // foo


字符串
---

### 字符串拼接用 join(), 避免使用 + 或 += 的方式拼接較長的字符串

應使用數組保存字符串片段,使用時調用 join 方法。避免使用 +  += 的方式拼接較長的字符串,每個字符串都會使用一個小的內存片段,過多的內存片段會影響性能

### 時間字符串比較 (時間形式注意補 0)

比較時間先後順序可以使用字符串:

var a = "2014-08-08"; var b = "2014-09-09";   console.log(a>b, a<b); // false true console.log("21:00"<"09:10");  // false console.log("21:00"<"9:10");   // true   時間形式注意補0


因爲字符串比較大小是按照字符串從左到右每個字符的 charCode 來的,但所以特別要注意時間形式注意補 0

數字
--

### 不同進製表示法

ES6 中新增了不同進制的書寫格式,在後臺傳參的時候要注意這一點。

29            // 10進制 035            // 8進制29      原來的方式 0o35            // 8進制29      ES6的方式 0x1d            // 16進制29 0b11101            // 2進制29


### 精確到指定位數的小數

將數字四捨五入到指定的小數位數。使用 Math.round() 和模板字面量將數字四捨五入爲指定的小數位數。省略第二個參數 decimals ,數字將被四捨五入到一個整數。

const round = (n, decimals = 0) => Number(${Math.round(${n}e${decimals})}e-${decimals}) round(1.345, 2)                 // 1.35   Number(1.345e2e-2) round(1.345, 1)                 // 1.3

// 此處e2表示乘以10的2次方  1.23e1   //12.3 1.23e2   // 123 123.45e-2  // 1.2345

數字補 0 操作

比如顯示時間的時候有時候會需要把一位數字顯示成兩位,這時候就需要補 0 操作,可以使用 slice 和 string 的 padStart 方法

const addZero1 = (num, len = 2) =(`0${num}`).slice(-len)    // 從字符串倒數第len處開始截取到最後
const addZero2 = (num, len = 2) =(`${num}`).padStart( len   , '0')   // padStart爲ES6的字符串拼接方法
addZero1(3) // 03
 
addZero2(32,4)  // 0032

此處可以採用另外一個思路;可以將得到的字符串先拼接len個0,然後再截取len長的字符串

數組

統計數組中相同項的個數

很多時候,你希望統計數組中重複出現項的個數然後用一個對象表示。那麼你可以使用 reduce 方法處理這個數組。

下面的代碼將統計每一種車的數目然後把總數用一個對象表示。

var cars = ['BMW','Benz''Benz''Tesla''BMW''Toyota'];
var carsObj = cars.reduce(function (obj, name) {
  obj[name] = obj[name] ? ++obj[name] : 1;    // obj[name]存在就加一,不存在就爲1
  return obj;
}{});
carsObj; // ={ BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }

交換參數數值

有時候你會將函數返回的多個值放在一個數組裏。我們可以使用數組解構來獲取其中每一個值。

let param1 = 1;
let param2 = 2;
[param1, param2] = [param2, param1];
console.log(param1) // 2
console.log(param2) // 1

當然我們關於交換數值有不少其他辦法:

var temp = a; a = b; b = temp            
b = [a, a = b][0]                     
a = a + b; b = a - b; a = a - b

### 批量接收多個請求返回結果

在下面的代碼中,我們從 / post 中獲取一個帖子,然後在 / comments 中獲取相關評論。由於我們使用的是 async/await,函數把返回值放在一個數組中。而我們使用數組解構後就可以把返回值直接賦給相應的變量。

async function getFullPost(){   return await Promise.all([      fetch('/post'),      fetch('/comments')   ]); } const [post, comments] = getFullPost();


### 將數組平鋪到指定深度

*   基礎的可以使用 **[].flat()** 方法來拉平單層數組, 代碼如下

let a1 = [{a:1},{a:2},[{a:3},{a:4},[{a:5}]]] a1.flat()  // [{a:1},{a:2},{a:3},{a:4},[{a:5}]]


*   使用遞歸,爲每個深度級別 depth 遞減 1 。使用 Array.reduce() 和 Array.concat() 來合併元素或數組。基本情況下,depth 等於 1 停止遞歸。省略第二個參數,depth 只能平鋪到 1 (單層平鋪) 的深度。

const flatten = (arr, depth = 1) =>   depth != 1     ? arr.reduce((a, v) => a.concat(Array.isArray(v) ? flatten(v, depth - 1) : v), [])     : arr.reduce((a, v) => a.concat(v), []); flatten([1, [2], 3, 4]);                             // [1, 2, 3, 4] flatten([1, [2, [3, [4, 5], 6], 7], 8], 2);           // [1, 2, 3, [4, 5], 6, 7, 8]

數組的對象解構

數組也可以對象解構,可以方便的獲取數組的第 n 個值

const csvFileLine = '1997,John Doe,US,john@doe.com,New York';
const { 2: country, 4: state } = csvFileLine.split(',');
 
country            // US
state            // New Yourk

對象數組按照某個屬性查詢最大值

var array=[
        {
            "index_id": 119,
            "area_id""18335623",
            "name""滿意度",
            "value""100"
        },
        {
            "index_id": 119,
            "area_id""18335624",
            "name""滿意度",
            "value""20"
        },
        {
            "index_id": 119,
            "area_id""18335625",
            "name""滿意度",
            "value""80"
        }];
        
 // 一行代碼搞定
Math.max.apply(Math, array.map(function(o) {return o.value}))

執行以上一行代碼可返還所要查詢的array數組中對象value屬性的最大值100。

同理,要查找最小值如下即可:Math.min.apply(Math, array.map(function(o) {return o.value})) 是不是比 for 循環方便了很多。

對象

使用解構刪除對象某個屬性

有時候你不希望保留某些對象屬性,也許是因爲它們包含敏感信息或僅僅是太大了。你可能會枚舉整個對象然後刪除它們,但實際上只需要簡單的將這些無用屬性賦值給變量,然後把想要保留的有用部分作爲剩餘參數就可以了。

下面的代碼裏,我們希望刪除_internal 和 tooBig 參數。我們可以把它們賦值給 internal 和 tooBig 變量,然後在 cleanObject 中存儲剩下的屬性以備後用。

let {_internal, tooBig, ...cleanObject} = {el1: '1', _internal:"secret", tooBig:{}, el2: '2', el3: '3'};
 
console.log(cleanObject);    // {el1: '1', el2: '2', el3: '3'}

解構嵌套對象屬性

在下面的代碼中,engine 是對象 car 中嵌套的一個對象。如果我們對 engine 的 vin 屬性感興趣,使用解構賦值可以很輕鬆地得到它。

var car = {
  model: 'bmw 2018',
  engine: {
    v6: true,
    turbo: true,
    vin: 12345
  }
}
const modelAndVIN = ({model, engine: {vin}}) ={
  console.log(`model: ${model} vin: ${vin}`);
}
modelAndVIN(car); // => model: bmw 2018  vin: 12345

代碼複用

Object [key]

雖然將 foo.bar 寫成 foo ['bar'] 是一種常見的做法,但是這種做法構成了編寫可重用代碼的基礎。許多框架使用了這種方法,比如 element 的表單驗證。

請考慮下面這個驗證函數的簡化示例:

function validate(values) {
  if(!values.first)
    return false;
  if(!values.last)
    return false;
  return true;
}
console.log(validate({first:'Bruce',last:'Wayne'})); // true

上面的函數完美的完成驗證工作。但是當有很多表單,則需要應用驗證,此時會有不同的字段和規則。如果可以構建一個在運行時配置的通用驗證函數,會是一個好選擇。

###Object [key] 實現表單驗證

// object validation rules
const schema = {
  first: {
    required:true
  },
  last: {
    required:true
  }
}
 
// universal validation function
const validate = (schema, values) ={
  for(field in schema) {
    if(schema[field].required) {
      if(!values[field]) {
        return false;
      }
    }
  }
  return true;
}
console.log(validate(schema, {first:'Bruce'})); // false
console.log(validate(schema, {first:'Bruce',last:'Wayne'})); // true

保持函數單一職責,靈活組合

保持函數的單一職責,保證一個函數只執行一個動作,每個動作互不影響,可以自由組合,就可以提高代碼的複用性。

比如下面的代碼,從服務端請求回來的訂單數據如下,需要進行以下三個處理:1. 根據 status 進行對應值得顯示(0 - 進行中,1 - 已完成,2 - 訂單異常) 2. 把 startTime 由時間戳顯示成 yyyy-mm-dd 3. 如果字段值爲空字符串 ,設置字段值爲 ‘--’

方案一:最基本的直接循環一次,同時操作三步

let orderList=[
    {
        id:1,
        status:0,
        startTime:1538323200000,
    },
    {
        id:2,
        status:2,
        startTime:1538523200000,
    },
    {
        id:3,
        status:1,
        startTime:1538723200000,
    },
    {
        id:4,
        status:'',
        startTime:'',
    },
];
需求似乎很簡單,代碼也少
let _status={
    0:'進行中',
    1:'已完成',
    2:'訂單異常'
}
orderList.forEach(item=>{
    //設置狀態
    item.status=item.status.toString()?_status[item.status]:'';
    //設置時間
    item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(/\//g,'-'):'';
    //設置--
    for(let key in item){
        if(item[key]===''){
            item[key]='--';
        }
    }
})

運行結果也正常,但是這樣寫代碼重複性會很多,

比如下面,另一組服務端請求回來的用戶數據,用戶數據沒有 status,startTime,兩個字段,而需要根據 type 對應顯示用戶的身份(0 - 普通用戶,1-vip,2 - 超級 vip)。

let userList=[
    {
        id:1,
        name:'守候',
        type:0
    },
    {
        id:2,
        name:'浪跡天涯',
        type:1
    },
    {
        id:3,
        name:'曾經',
        type:2
    }
]
出現這樣的需求,之前寫的代碼無法重用,只能複製過來,再修改下。
let _type={
    0:'普通用戶',
    1:'vip',
    2:'超級vip'
}
userList.forEach(item=>{
    //設置type
    item.type=item.type.toString()?_type[item.type]:'';
    //設置--
    for(let key in item){
        if(item[key]===''){
            item[key]='--';
        }
    }
})

結果正常,想必大家已經發現問題了,代碼有點多餘。下面就使用單一職責的原則改造下操作函數,設置 status,startTime,type,-- 。這裏拆分成四個函數。

方案二:單一職責,拆分爲多個函數,導致循環遍歷次數增加

let handleFn={
    setStatus(list){
        let _status={
            0:'進行中',
            1:'已完成',
            2:'訂單異常'
        }
        list.forEach(item=>{
            item.status=item.status.toString()?_status[item.status]:'';
        })
        return list
    },
    setStartTime(list){
        list.forEach(item=>{
            item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(/\//g,'-'):'';
        })
        return list;
    },
    setInfo(list){
        list.forEach(item=>{
            for(let key in item){
                if(item[key]===''){
                    item[key]='--';
                }
            }
        })
        return list;
    },
    setType(list){
        let _type={
            0:'普通用戶',
            1:'vip',
            2:'超級vip'
        }
        list.forEach(item=>{
            item.type=item.type.toString()?_type[item.type]:'';
        })
        return list;
    }
}
下面直接調用函數就好
//處理訂單數據
orderList=handleFn.setStatus(orderList);
orderList=handleFn.setStartTime(orderList);
orderList=handleFn.setInfo(orderList);
console.log(orderList);
//處理用戶數據
userList=handleFn.setType(userList);
userList=handleFn.setInfo(userList);
console.log(userList);

運行結果也正常, 但是性能上由於循環次數變多會犧牲一點性能,但是如果數據不多的話是可以接受的

如果嫌棄連續賦值麻煩,可以借用 jQuery 的那個思想,進行鏈式調用。方案三:多次調用函數比較煩,此處採用鏈式調用

let ec=(function () {
    let handle=function (obj) {
        //深拷貝對象
        this.obj=JSON.parse(JSON.stringify(obj));
    };
    handle.prototype={
        /**
         * @description 設置保密信息
         */
        setInfo(){
            this.obj.map(item=>{
                for(let key in item){
                    if(item[key]===''){
                        item[key]='--';
                    }
                }
            });
            return this;
        },
        /**
         * @description 設置狀態
         */
        setStatus(){
            let _status={
                0:'進行中',
                1:'已完成',
                2:'訂單異常'
            }
            this.obj.forEach(item=>{
                item.status=item.status.toString()?_status[item.status]:''
            });
            return this;
        },
        /**
         * @description 設置時間
         */
        setStartTime(){
               this.obj.forEach(item=>{
                item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(/\//g,'-'):'';
            });
            return this;
        },
        /**
         * @description 設置type
         */
        setType(){
            let _type={
                0:'普通用戶',
                1:'vip',
                2:'超級vip'
            }
            this.obj.forEach(item=>{
                item.type=item.type.toString()?_type[item.type]:'';
            })
            return this;
        },
        /**
         * @description 返回處理結果
         * @return {Array|*}
         */
        end(){
            return this.obj;
        }
    }
    //暴露構造函數接口
    return function (obj) {
        return new handle(obj);
    }
})();
這樣就可以鏈式調用了
//處理訂單數據
orderList=ec(orderList).setStatus().setStartTime().setInfo().end();
console.log(orderList);
//處理用戶數據
userList=ec(userList).setType().end();
console.log(userList);

事情到這裏了,相信大家發現一個很嚴重的問題就是循環的次數增加了。沒優化之前,只需要循環一次,就可以把設置狀態,設置時間,設置 -- 這些步驟都完成, 但是現在 setStatus().setStartTime().setInfo() 這裏的代碼,每執行一個函數,都遍歷了一次數組。

方案四:優化方案,在每一個函數里面,只記錄要處理什麼,但是不進行處理,等到執行到 end 的時候再統一處理,以及返回

let orderList=[
  {
   id:1,
   status:0,
   startTime:1538323200000,
  },
  {
   id:2,
   status:2,
   startTime:1538523200000,
  },
  {
   id:3,
   status:1,
   startTime:1538723200000,
  },
  {
   id:4,
   status:'',
   startTime:'',
  },
 ];
let userList=[
  {
   id:1,
   name:'守候',
   type:0
  },
  {
   id:2,
   name:'浪跡天涯',
   type:1
  },
  {
   id:3,
   name:'曾經',
   type:2
  }
 ]

let ec=(function () {
  let handle=function (obj) {
   //深拷貝對象
   this.obj=JSON.parse(JSON.stringify(obj));
   //記錄要處理的步驟
   this.handleFnList=[];
  };
  handle.prototype={
   /**
    * @description 設置保密信息
    */
   handleSetInfo(item){
    for(let key in item){
     if(item[key]===''){
      item[key]='--';
     }
    }
    return this;
   },
   setInfo(){
    this.handleFnList.push('handleSetInfo');
    return this;
   },
   /**
    * @description 設置狀態
    */
   handleSetStatus(item){
    let _status={
     0:'進行中',
     1:'已完成',
     2:'訂單異常'
    }
    item.status=item.status.toString()?_status[item.status]:''
    return item;
   },
   setStatus(){
    this.handleFnList.push('handleSetStatus');
    return this;
   },
   /**
    * @description 設置時間
    */
   handleSetStartTime(item){
    item.startTime=item.startTime.toString()?new Date(item.startTime).toLocaleDateString().replace(/\//g,'-'):'';
    return item;
   },
   setStartTime(){
    this.handleFnList.push('handleSetStartTime');
    return this;
   },
   /**
    * @description 設置type
    */
   handleSetType(item){
    let _type={
     0:'普通用戶',
     1:'vip',
     2:'超級vip'
    }
    item.type=item.type.toString()?_type[item.type]:'';
    return item;
   },
   setType(){
    this.handleFnList.push('handleSetType');
    return this;
   },
   /**
    * @description 返回處理結果
    * @return {Array|*}
    */
   end(){
    //統一處理操作
    this.obj.forEach(item=>{
     this.handleFnList.forEach(fn=>{
      item=this[fn](item);    // 依次執行handleFnList隊列中的函數,並傳入item作爲參數
     })
    })
    return this.obj;
   }
  }
  //暴露構造函數接口
  return function (obj) {
   return new handle(obj);
  }
 })();
    // 調用
 //處理訂單數據
 orderList=ec(orderList).setStatus().setStartTime().setInfo().end();
 console.log(orderList);
 //處理用戶數據
 userList=ec(userList).setType().end();
 console.log(userList);
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RVX7IzbuKDL8quWlbew9jQ