[fed-task-01-02]函數式編程與 JS 性能優化

簡答題

1.描述引用計數的工做原理和優缺點。

引用計數的工做原理:設置對象的引用數,有一個引用計數器來維護這些引用數,引用關係改變時修改引用數。判斷當前對象引用數是否爲0,引用數爲0時當即回收。javascript

引用計數算法優勢:java

  • 發現垃圾時當即回收
  • 最大限度減小程序暫停

引用計數算法缺點:web

  • 沒法回收循環引用的對象
  • 時間開銷大,資源消耗較大

2.描述標記整理算法的工做流程。

標記整理算法工做流程:算法

  • 標記整理能夠看作是標記清除的加強,也是分標記和清除兩個階段來完成
  • 標記階段:遍歷全部對象找標記活動對象
  • 清除階段:先執行整理,移動對象的位置,而後遍歷全部對象清除沒有標記的對象
  • 最後回收相應的空間

標記整理算法優缺點:數據庫

  • 減小碎片化空間
  • 不會當即回收垃圾對象

3.描述 V8 中新生代存儲區垃圾回收的流程。

新生代存儲區垃圾回收流程:編程

  • 回收過程採用複製算法 + 標記整理
  • 新生代內存區分爲兩個等大小空間 From 和 To
  • 使用空間爲 From,空閒空間爲 To
  • 活動對象存儲在 From 空間,標記整理後將活動對象拷貝至 To
  • 拷貝過程當中可能出現晉升(晉升就是將新生代對象移動至老生代)。
  • 一輪 GC 還存活的新生代須要晉升;To 空間使用率超過 25%,也要將活動對象移動至老生代。
  • 最後將 From 與 To 交換空間完成內存釋放

4.描述增量標記算法在什麼時候使用,以及工做原理。

增量標記算法:將一整段的垃圾回收操做,拆分紅多個小步,組合完成整個垃圾回收操做。咱們知道,當垃圾回收工做的時候,會阻塞JS程序執行,當咱們須要優化垃圾回收的效率時,就可使用增量標記算法。數組

優勢:讓垃圾回收與程序執行能夠交替完成,讓時間消耗更合理,達到效率優化的好處。瀏覽器

工做原理:緩存

  • JS 程序執行的過程當中,會伴隨着垃圾回收的工做
  • 當垃圾回收工做時,須要遍歷對象進行標記,此時不須要將全部對象進行標記,能夠先將直接可達的對象進行標記,此時停下標記操做
  • 而後讓JS程序執行一會,以後,再讓 GC 機制去作二步的標記操做,去標記那些間接可達的對象
  • 重複以上兩步,讓程序執行和垃圾回收的標記操做交替執行,來達到優化效率和提高用戶體驗的目的
  • 直到標記操做完成以後,最後執行垃圾回收

代碼題1

基於如下代碼完成下面的練習性能優化

const fp = require('lodash/fp');

// 數據
// horsepower 馬力,dollar_value 價格, in_stock 庫存 
const cars = [
    { name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true }, 
    { name: "Spyker Cl2 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false },
    { name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false } ,
    { name: "Audi R8", horsepower: 525, dollar_value: 114200, in_stock: false }, 
    { name: "Aston Martin One-77", horsepower: 750, dollar_value: 1850000, in_stock: true },
    { name: "Pagani Huayra" , horsepower: 700, dollar_value: 1300000, in_stock: false }
];

練習1

使用函數組合 fp.flowRight() 從新實現下面這個函數

let isLastInStock = function(cars) {
    // 獲取最後一條數據
    let last_car = fp.last(cars);
    // 獲取最後一條數據的 in_stock 屬性值
    return fp.prop('in_stock', last_car);
}

答:

let isLastInStock = fp.flowRight(fp.prop('in_stock'), fp.last)
console.log(isLastInStock(cars)); // false

練習2

使用 fp.flowRight(), fp.prop() 和 fp.first() 獲取第一個 car 的 name

答:

let getFirstCarName = fp.flowRight(fp.prop('name'), fp.first);
console.log(getFirstCarName(cars)); // Ferrari FF

練習3

使用幫助函數 _average 重構 averageDollarValue ,使用函數組合的方式實現

// _average 無需改動
let _average = function(xs) {
    return fp.reduce(fp.add, 0, xs) / xs.length;
}

let averageDollarValue = function(cars) {
    let dollar_values = fp.map(function(car) {
        return car.dollar_value
    }, cars);
    return _average(dollar_values);
}

答:

let averageDollarValue = fp.flowRight(_average, fp.map(car => car.dollar_value));
console.log(averageDollarValue(cars)); // 790700

練習4

使用 flowRight 寫一個 sanitizeNames() 函數,返回一個下劃線鏈接的小寫字符串,把數組中的 name 轉換爲這種形式:例如:sanitizeNames(["Hello World"]) => ["hello_world"]

let _underscore = fp.replace(/\W+/g, '_'); // 無需改動,並在 sanitizeNames 中使用它

答:

let sanitizeNames = fp.flowRight(fp.map(fp.flowRight(_underscore, fp.toLower)));

console.log(sanitizeNames(["Hello World"]));
// [ 'hello_world' ]

console.log(sanitizeNames(["Hello World", "I am Lxcan"]));
// [ 'hello_world', 'i_am_lxcan' ]

代碼題2

基於下面提供的代碼,完成後續的練習

// support.js

class Container {
    static of (value) {
        return new Container(value);
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return Container.of(fn(this._value));
    }
}

class Maybe {
    static of (x) {
        return new Maybe(x);
    }
    isNothing () {
        return this._value === null || this._value === undefined;
    }
    constructor (x) {
        this._value = x;
    }
    map (fn) {
        return this.isNothing() ? this : Maybe.of(fn(this._value));
    }
}

module.exports = {
    Maybe,
    Container
}

練習1

使用 fp.add(x, y) 和 fp.map(f, x) 建立一個能讓 functor 裏的值增長的函數 ex1

const fp = require('lodash/fp');
const { Maybe, Container } = require('./support.js');

let maybe = Maybe.of([5, 6, 1]);
// let ex1 = // ... 你須要實現的位置

答:

// let ex1 = fp.flowRight(fp.map(v => fp.add(v, 1)));
// console.log(maybe.map(ex1));
// Maybe { _value: [ 6, 7, 2 ] }

let ex1 = n => maybe.map(arr => fp.map(v => fp.add(v, n), arr));
console.log(ex1(1)); // 數組每一項加1
// Maybe { _value: [ 6, 7, 2 ] }

練習2

實現一個函數 ex2,可以使用 fp.first 獲取列表的第一個元素

const fp = require('lodash/fp');
const { Maybe, Container } = require('./support.js');

let xs = Container.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do']);
// let ex2 = // ... 你須要實現的位置

答:

let ex2 = fn => xs.map(fn)._value;
console.log(ex2(fp.first)); // "do"

練習3

實現一個函數 ex3 ,使用 safeProp 和 fp.first 找到 user 的名字的首字母

const fp = require('lodash/fp');
const { Maybe, Container } = require('./support.js');

let safeProp = fp.curry(function (x, o) {
    return Maybe.of(o[x]);
});
let user = { id: 2, name: 'Albert' };
// let ex3 = // ... 你須要實現的位置

答:

let ex3 = () => safeProp('name', user).map(fp.first)._value;
console.log(ex3()); // A

練習4

使用 Maybe 重寫 ex4 ,不要有 if 語句

const fp = require('lodash/fp');
const { Maybe, Container } = require('./support.js');

let ex4 = function (n) {
    if (n) {
        return parseInt(n)
    }
}

答:

let ex4 = n => Maybe.of(n).map(parseInt)._value;
console.log(ex4('100')); // 100

學習筆記

函數式編程

函數式編程就是對運算過程的抽象,其中的函數是指數學中的函數即映射關係,相同的輸入始終要獲得相同的輸出(純函數)。

函數是一等公民

  • 函數能夠存儲在變量中
  • 函數做爲參數
  • 函數做爲返回值

函數是一等公民是咱們後面要學習的高階函數、柯里化等的基礎

使用高階函數的意義:

  • 抽象能夠幫咱們屏蔽細節,只須要關注於咱們的目標
  • 高階函數是用來抽象通用的問題

閉包

能夠在另外一個做用域中調用一個函數的內部函數並訪問到該函數做用域中的成員

閉包的本質:函數在執行的時候會放到一個執行棧上,當函數執行完畢以後會從執行棧上移除,可是堆上的做用域成員由於被外部引用不能釋放,所以內部函數依然能夠訪問外部函數的成員。

純函數

相同的輸入永遠會獲得相同的輸出,並且沒有任務可觀察的反作用。

純函數的好處:

  • 可緩存,純函數對相同的輸入始終有相同的結果,因此能夠吧結果緩存起來
  • 可測試,純函數讓測試更方便
  • 並行處理,純函數不須要訪問共享的內存數據,因此在並行環境下能夠任意運行純函數

反作用

反作用讓一個函數變得不純,若是函數依賴於外部的狀態就沒法保證輸出相同,就會帶來反作用。

反作用來源:

  • 全局變量
  • 配置文件
  • 數據庫
  • 獲取用戶的輸入
  • ...

柯里化(Currying)

  • 當一個函數有多個參數的時候先傳遞一部分參數調用它(這部分參數之後永遠不變)
  • 而後返回一個新的函數接收剩餘的參數,返回結果

總結:

  • 柯里化可讓咱們給一個函數傳遞較少的參數,獲得一個已經記住了某些固定參數的新函數
  • 這是一種對函數參數的「緩存」
  • 讓函數變得更加靈活,讓函數的粒度更小
  • 能夠把多元函數轉換成一元函數,能夠組合使用函數產生強大的功能

函數組合

函數組合(compose):若是一個函數要通過多個函數處理才能獲得最終值,這個時候能夠把中間過程的函數合併成一個函數

  • 函數就像是數據的管道,函數組合就是把這些管道鏈接起來,讓數據穿過多個管道造成最終結果
  • 經過函數組合能夠把多個一元函數組合成一個功能更強大的函數
  • 函數組合默認是從右到左執行
  • 函數組合須要知足結合律

函數組合要知足結合律(associativity)

咱們既能夠把 g 和 h 組合,還能夠把 f 和 g 組合,結果都是同樣的

let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h));
// true

lodash/fp

  • lodash 的 fp 模塊提供了實用的對 函數式編程友好 的方法
  • 提供了不可變 auto-curried , iteratee-first, data-last 的方法

PointFree

咱們能夠把數據處理的過程定義成與數據無關的合成運算,不須要用到表明數據的那個參數,只要把簡單的運算步驟合成到一塊兒,在使用這種模式以前咱們須要定義一些輔助的基本運算函數。

  • 不須要指明處理的數據
  • 只須要合成運算過程
  • 須要定義一些輔助的基本運算函數

Functor(函子)

什麼是函子?

  • 容器:包含值和值的變形關係(這個變形關係就是函數)
  • 函子:是一個特殊的容器,經過一個普通的對象來實現,該對象具備 map 方法,map 方法能夠運行一個函數對值進行處理(變形關係)

MayBe 函子

  • 咱們在編程的過程當中可能會遇到不少錯誤,須要對這些錯誤作相應的處理
  • Maybe 函子的做用就是能夠對外部的空值狀況作處理(控制反作用在容許的範圍)

Either 函子

  • Either 二者中的任何一個,相似於 if...else... 的處理
  • 異常會讓函數變的不純,Either 函子能夠用來作異常處理

IO 函子

  • IO 函子中的 _value 是一個函數,這裏是把函數做爲值來處理
  • IO 函子能夠把不純的動做存儲到 _value 中,延遲執行這個不純的操做(惰性執行)
  • 包裝當前的操做,把不純的操做交給調用者來處理

folktale

  • 一個標準的函數式編程庫
  • 和 lodash、ramda 不一樣的是,他沒有提供不少功能函數
  • 只提供了一些函數式處理的操做,例如:compose、curry 等,一些函子 Task,Either,MayBe 等
  • task 異步執行

Pointed 函子

  • Pointed 函子是實現了 of 靜態方法的函子
  • of 方法是爲了不使用 new 來建立對象,更深層的含義是 of 方法用來把值放到上下文 Context 中(把值放到容器中,使用 map 來處理值)

Monad 函子

  • Monad 函子是能夠變扁的 Pointed 函子,IO(IO(x))
  • 一個函子若是具備 join 和 of 兩個方法,並遵照一些定律就是一個 Monad

JavaScript 性能優化

內存管理

介紹

  • 內存:由可讀寫單元組成,表示一片可操做空間
  • 管理:人爲的去操做一片空間的申請、使用和釋放
  • 內存管理:開發者主動申請空間、使用空間、釋放空間
  • 管理流程:申請-使用-釋放

垃圾回收與常見 GC 算法

JavaScript 中的垃圾

  • JS 中的內存管理是自動的
  • 對象再也不被引用時是垃圾
  • 對象不能從根上訪問到時是垃圾

JavaScript 中的可達對象

  • 能夠訪問到的對象就是可達對象(引用、做用域鏈)
  • 可達的標準就是從根出發是否可以被找到
  • JS 中的根就能夠理解爲是全局變量對象

引用計數算法

  • 核心思想:設置引用數,判斷當前對象引用數是否爲0
  • 引用計數器
  • 引用關係改變時修改引用數字
  • 引用數字爲0時當即回收

引用計數算法優勢:

  • 發現垃圾時當即回收
  • 最大限度減小程序暫停

引用計數算法缺點:

  • 沒法回收循環引用的對象
  • 時間開銷大,資源消耗較大

標記清除算法

  • 核心思想:分標記和清除兩個階段完成
  • 遍歷全部對象找標記活動對象
  • 遍歷全部對象清除沒有標記的對象
  • 回收相應的空間

標記清除算法優勢:

  • 相對於引用計數算法,它能夠回收循環引用的對象

標記清除算法缺點:

  • 它釋放的空間是分散的,地址不連續
  • 會產生空間碎片化的問題,浪費空間
  • 不會當即回收垃圾對象

標記整理算法

  • 標記整理能夠看作是標記清除的加強
  • 標記階段的操做和標記清除一致
  • 清除階段會先執行整理,移動對象的位置

標記整理算法優缺點:

  • 減小碎片化空間
  • 不會當即回收垃圾對象

V8 引擎的垃圾回收

認識V8

  • V8 是一款主流的 JavaScript 執行引擎
  • V8 採用即時編譯,速度很快
  • V8 內存設限,64位操做系統最大1.5G,32位操做系統最大800M

V8 垃圾回收策略

  • 採用分代回收的思想
  • 內存分爲新生代、老生代
  • 針對不一樣對象採用不一樣算法

V8 中經常使用的 GC(Garbage Collection) 算法

  • 分代回收、空間複製、標記清除、標記整理、標記增量

V8 內存分配

  • V8 內存空間一分爲二
  • 小空間用於存儲新生代對象(64位操做系統:32M,32位操做系統:16M)
  • 新生代指的是存活時間較短的對象

新生代對象回收實現

  • 回收過程採用複製算法 + 標記整理
  • 新生代內存區分爲兩個等大小空間 From 和 To
  • 使用空間爲 From,空閒空間爲 To
  • 活動對象存儲在 From 空間,標記整理後將活動對象拷貝至 To
  • From 與 To 交換空間以後完成內存釋放

回收細節說明

  • 拷貝過程當中可能出現晉升
  • 晉升就是將新生代對象移動至老生代
  • 一輪 GC 還存活的新生代須要晉升
  • To 空間的使用率超過 25%,也要將活動對象移動至老生代

老生代對象說明

  • 老生代對象存放在右側的老生代區域
  • 64位操做系統 1.4G,32位操做系統 700M
  • 老生代對象就是指存活時間較長的對象

老生代對象回收實現

  • 主要採用標記清除、標記整理、增量標記算法
  • 首先使用標記清除完成垃圾空間的回收
  • 採用標記整理進行空間優化
  • 採用增量標記進行效率優化

細節對比

  • 新生代區域垃圾回收使用空間換時間,會浪費一部分空間
  • 老生代區域垃圾回收不適合複製算法

V8 總結

  • V8 是一款主流的 JavaScript 執行引擎
  • V8 內存設置上限(從web應用、用戶體驗考慮)
  • V8 採用基於分代回收思想實現垃圾回收
  • V8 內存分爲新生代和老生代
  • V8 垃圾回收常見的 GC 算法

Performance 工具

爲何使用 Performance

  • GC 的目的是爲了實現內存空間的良性循環
  • 良性循環的基石是合理使用
  • 時刻關注才能肯定是否合理
  • Performance 提供多種監控方式

Performance 使用步驟

  • 打開瀏覽器輸入目標網址
  • 進入開發人員工具面板,選擇性能
  • 開啓錄製功能,訪問具體界面
  • 執行用戶行爲,一段時間後中止錄製
  • 分析界面中記錄的內存信息

內存問題的外在表現

  • 頁面出現延遲加載或常常性暫停
  • 頁面持續性出現糟糕的性能
  • 頁面的性能隨時間延長愈來愈差

界定內存問題的標準

  • 內存泄漏:內存使用持續升高
  • 內存膨脹:在多數設備上都存在性能問題
  • 頻繁垃圾回收:經過內存變化圖進行分析

監控內存的幾種方式

  • 瀏覽器任務管理器
  • Timeline 時序圖記錄
  • 堆快照查找分離 DOM
  • 判斷是否存在頻繁的垃圾回收

如何肯定存在頻繁的垃圾回收?

  • Timeline 中頻繁的上升降低
  • 任務管理器中數據頻繁的增長減少

代碼優化實例

如何精準測試 JavaScript 性能

  • 本質上就是採集大量的執行樣本進行數學統計和分析
  • 使用基於 Benchmark.js 的 https://jsperf.com/ 完成

jsperf 使用流程

  • 使用 GitHub 帳號登陸
  • 填寫我的信息(非必須)
  • 填寫詳細的測試用例信息 (title,slug)
  • 填寫準備代碼(DOM操做時常用)
  • 填寫必要的 setup 與 teardown 代碼
  • 填寫測試代碼片斷

慎用全局變量

  • 全局變量定義在全局執行上下文,是全部做用域鏈的頂端
  • 全局執行上下文一直存在於上下文執行棧,直到程序退出
  • 若是某個局部做用域出現了同名變量則會覆蓋或污染全局
相關文章
相關標籤/搜索