4

20190315103011.png

[译] 理解 JavaScript Mutation 突变和 PureFunction 纯函数

不可变性、纯函数、副作用,状态可变这些单词我们几乎每天都会见到,但我们几乎不知道他们是如何工作的,以及他们是什么,他们为软件开发带来了什么好处。

在这篇文章中,我们将深入研究所有这些,以便真正了解它们是什么以及如何利用它们来提高我们的Web应用程序的性能。

Javascript:原始数据类型和引用数据类型

我们将首先了解JS如何维护以及访问到我们的数据类型。

在JS中,有原始数据类型和引用数据类型。原始数据类型由值引用,而非原始/引用数据类型指向内存地址。

原始数据类型是:

  • Boolean
  • Number
  • String
  • Null
  • Undefined
  • Symbol

引用数据类型:

  • Object
  • Arrays

当我们写原始数据类型时是这个样子:

let one = 1

在调用堆栈中,one 变量直接指向值 1:

Call Stack
#000    one -> | 1 |
#001           |   |
#002           |   |
#003           |   |

如果我们改变这个值:

let one = 1
one = 3

变量 one 的内存地址 #000 原本存储 1 这个值,会直接变成 3

但是,如果我们像这样写一个引用数据类型:

let arr = {
    one: 1
}

或:

let arr = new Object()
arr.one = 1

JS将在内存的堆中创建对象,并将对象的内存地址存储在堆上:

Call Stack       Heap
#000    arr -> | #101 |   #101 | one: 1 |
#001           |      |   #102 |        |
#002           |      |   #103 |        |
#003           |      |   #104 |        |

看到 arr 不直接存储对象,而是指向对象的内存位置(#101)。与直接保存其值的原始数据类型不同。

let arr = { one: 1 }
// arr holds the memory location of the object {one: 1}
// `arr` == #101
let one = 1;
// `one` a primitive data type holds the value `1`
// one == 1

如果我们改变 arr 中的属性,如下所示:

arr.one = 2

那么基本上我们就是在告诉程序更改 arr 对对象属性值的指向。如果你对 C/C++ 等语言的指针和引用比较熟悉,那么这些你都会很容易理解。

传递引用数据类型时,你只是在传递其内存位置的递值,而不是实际的值。

function chg(arg) {
    //arg points to the memory address of { one: 1 }
    arg.one = 99
    // This modification will affect { one: 1 } because arg points to its memory address, 101
}
let arr = { one: 1 } 
// address of `arr` is `#000`
// `arr` contains `#101`, adrress of object, `{one: 1}` in Heap
log(arr); // { one: 1 }
chg(arr /* #101 */)
// #101 is passed in
log(arr) // { one: 99 }
// The change affected `arr`
译者注:arr 本身的内存地址是 #000;arr 其中保存了一个地址 #101;这个地址指向对象 {one:1};在调用 chg 函数的时候,那么修改 arg 属性 one 就会修改 arr 对应的 #101 地址指向的对象 {one:1}

因为引用数据类型保存的是内存地址,所以对他的任何修改都会影响到他指向的内存。

如果我们传入一个原始数据类型:

function chg(arg) {
    arg++
}
let one = 1; // primitive data types holds the actual value of the variable.
log(one) // 1
chg(one /* 1 */)
// the value of `one` is passed in.
log(one) // one is still `1`. No change because primitives only hold the value
译者注:不像原始数据类型,他的值是多少就是多少如果修改了这个值,那么直接修改所在内存对应的这个值

状态突变和不可变性

在生物学领域,我们知道 DNA 以及 DNA 突变。DNA 有四个基本元素,分别是 ATGC。这些生成了编码信息,在人体内产生一种蛋白质。

ATATGCATGCGATA
||||||||||||||   
TACGAGCTAGGCTA
|
|
v
AProteinase
Information to produce a protein (eg, insulin etc)

上述DNA链编码信息以产生可用于骨结构比对的AP蛋白酶蛋白。

如果我们改变DNA链配对,即使是一对:

ATATGCATGCGATA
||||||||||||||   
TACGAGCTAGGCTA
|
v 
GTATGCATGCGATA
||||||||||||||   
TACGAGCTAGGCTA

DNA将产生不同的蛋白质,因为产生蛋白质AP蛋白酶的信息已经被篡改。因此产生了另一种蛋白质,其可能是良性的或在某些情况下是有毒的。

GTATGCATGCGATA
||||||||||||||   
TACGAGCTAGGCTA
|
|
V
Now produces _AProtienase

我们称这种变化突变DNA突变

突变引起DNA状态的改变。

而对于 JS 来说,引用数据类型(数组,对象)都被称为数据结构。这些数据结构保存信息,以操纵我们的应用程序。

let state = {
    wardens: 900,
    animals: 800
}

上面名为 state 的对象保存了 Zoo 应用程序的信息。如果我们改变了 animals 属性的值:

let state = {
    wardens: 900,
    animals: 800
 }
state.animals = 90

我们的 state 对象会保存或编码一个新的信息:

state = {
    wardens: 900,
    animals: 90    
}

这就叫突变 mutation

我们的 state 从:

state = {
    wardens: 900,
    animals: 800    
}

变为:

state = {
    wardens: 900,
    animals: 90    
}

当我们想要保护我们的 state 时候,这就需要用到不可变性了 immutability。为了防止我们的 state 对象发生变化,我们必须创建一个 state 对象的新实例。

function bad(state) {
    state.prp = 'yes'
    return state
}
function good(state) {
    let newState = { ...state }
    newState.prp = 'yes'
    return newState
}

不可变性使我们的应用程序状态可预测,提高我们的应用程序的性能速率,并轻松跟踪状态的变化。

纯函数和副作用

纯函数是接受输入并返回值而不修改其范围之外的任何数据的函数(副作用)。它的输出或返回值必须取决于输入/参数,纯函数必须返回一个值。

译者注:纯函数必须要满足的条件:不产生副作用、返回值只取决于传入的参数,纯函数必须返回一个值
function impure(arg) {
    finalR.s = 90
    return arg * finalR.s
}

上面的函数不是纯函数,因为它修改了其范围之外的状态 finalR.s

function impure(arg) {
    let f = finalR.s * arg
}

上面的函数也不是纯函数,因为虽然它没有修改任何外部状态,但它没有返回值。

function impure(arg) {
    return finalR.s * 3
}

上面的函数是不纯的,虽然它不影响任何外部状态,但它的输出返回 finalR.s * 3 不依赖于输入 arg。纯函数不仅必须返回一个值,还必须依赖于输入。

function pure(arg) {
    return arg * 4
}

上面的函数才是纯函数。它不会对任何外部状态产生副作用,它会根据输入返回输出。

能够带来的好处

就个人而言,我发现的唯一能够让人理解的好处是 mutation tracking 变异追踪。

知道何时渲染你的状态是非常重要的事情。很多 JS 框架设计了不错的方法来检测何时去渲染其状态。但是最重要的是,要知道在首次渲染完毕后,何时触发再渲染 re-render。这就被称为变异追踪了。这需要知道什么时候状态被改变了或者说变异了。以便去触发再渲染 re-render

于我们已经实现了不变性,我们确信我们的应用程序状态不会在应用程序中的任何位置发生变异,况且纯函数完全准寻其处理逻辑和原则(译者注:不会产生副作用)。这就很容易看出来到底是哪里出现变化了(译者注:反正不是纯函数也不是 immutable 变量)。

let state = {
    add: 0,
}
funtion render() {
    //...
}
function effects(state,action) {
    if(action == 'addTen') {
        return {...state, add: state.add + 10}
    }
    return state;
}
function shouldUpdate(s) {
    if(s === state){
        return false
    }
    return true
}
state = effects(state, 'addTen')
if(shouldUpdate(state)) {
    render();
}

这里有个小程序。这里有个 state 对象,对象只有一个属性 add。render 函数正常渲染程序的属性。他并不会在程序的任何改变时每次都触发渲染 state 对象,而是先检查 state 对象是否改变。

就像这样,我们有一个 effects 函数和一个纯函数,这两个函数都用来去修改我们的 state 对象。你会看到它返回了一个新的 state 对象,当要更改状态时返回新状态,并在不需要修改时返回相同的状态。

因此,我们有一个shouldUpdate函数,它使用===运算符检查旧状态和新状态是否相同。如果它们不同,则调用render函数,以更新新状态。

结论

我们研究了 Web 开发中这几个最常见的术语,并展示了它们的含义以及它们的用途。如果你付诸实践,这将是非常有益的。

如果有任何对于这篇文章的问题,如我应该增加、修改或删除,请随时评论、发送电子邮件或直接 DM 我。干杯 🙏


JS菌
6.4k 声望2k 粉丝