Javascript中的求值策略

前端雜貨鋪發表於2019-02-28
原文:zhuanlan.zhihu.com/p/33035557

介紹

最近看到一個關於JS函式引數傳值策略的討論。很多人會認為JS的Object型別作為函式引數是按引用傳遞,而基礎型別是按值傳遞,他們也提出了自己的佐證,而且網上搜尋很多文章好像也這麼說。但是這樣的說發是不是正確的呢?讓我們來探討一下JS裡面的求值策略。

維基百科搜尋Evaluate-Strategy你可以看到求值策略其實是程式語言裡面的一個常用術語。求值策略通常指對某種程式語言的表示式進行求值和計算的一個規則集。而函式引數的傳值策略是其中一個特殊的例子。

一般業界常見的求值策略有嚴格和非嚴格策略。在“嚴格求值”中,給函式的實參總是在這個函式被呼叫之前求值,相應的“非嚴格求值”就是在函式呼叫時求值所以也叫“惰性求值”。

和大部分語言(C,java,Python,Ruby等)一樣JS採用的也是嚴格求值策略,不同的是在JS裡面引數求值順序從左至右而其他的實現則是從右至左。

注:ES6裡面函式增加了預設引數,引數預設值不是傳值的,而是每次都重新計算預設值表示式的值也就是說,引數預設值是惰性求值的。

瞭解JS的函式傳參策略對於我們理解JS來說意義重大。

問題

在此之前我們先來看一下問題:

function magic(num, objectA, objectB) {
     num = num * 6;
     objectA = {name: 'AA'} 
     objectB.name = 'BB';
    }
    const num = 1;
    const objectA = {name: 'A'};
    const objectB = {name: 'B'};
    magic(num, objectA, objectB);
    console.log(num); // 1
    console.log(objectA); // {name: "A"}
    console.log(objectB); // {name: "BB"} something change
複製程式碼

JS紅寶書裡面說過這麼一句話: ECMAScript中所有函式的引數都是按值傳遞的,把函式外部的值複製給函式內部的引數,就和把值從一個變數複製到另一個變數一樣。 上述例子中傳入了三個值,一個Number型別,兩個引用型別。然而當我們objectB的一個屬性改變之後,居然改變了傳入的變數的值。到底咋回事?

Call By Value 按值傳遞

“傳值呼叫”求值是最常見的求值策略, 被求值了的引數值會被繫結當前函式的變數裡(也就是傳遞的是其值的拷貝)。此策略中,函式內部改變引數值不會影響到外部值,一般來說是給改引數值分配了新記憶體,並被函式內部呼叫。

Call By Reference 按引用傳遞

按引用傳遞接收的不是值拷貝,而是物件的隱式引用,如該物件在外部的直接引用地址。函式內部對引數的任何改變都是影響該物件在函式外部的值,因為兩者引用的是同一個物件,也就是說:這時候引數就相當於外部物件的一個別名。 讓我們來看一個PHP的例子:

<?php
function foo(&$var) // 這裡& 表示將引數按引用傳遞
{
    $var++;
}

$a=5;
foo($a);
// $a is 6 
?>
複製程式碼

Call By Sharing 按共享傳遞

這個策略還有一些代名詞:“按物件傳遞”或“按物件共享傳遞”,該策略是1974年由Barbara Liskov為CLU程式語言提出的。 該策略的要點是:函式接收的是物件對於的拷貝(副本),該引用拷貝和形參以及其值相關聯。 這裡出現的引用,我們不能稱之為“按引用傳遞”,因為函式接收的引數不是直接的物件別名,而是該引用地址的拷貝。 最重要的區別就是:函式內部給引數重新賦新值不會影響到外部的物件(和上例按引用傳遞的case),但是因為該引數是一個地址拷貝,所以在外面訪問和裡面訪問的都是同一個物件(例如外部的該物件不是想按值傳遞一樣完全的拷貝),改變該引數物件的屬性值將會影響到外部的物件。

解析

看到這裡或許你對上面那個問題有一些眉目了,我們就來解析一下: 在之前的文章 我們有講過JavaScript 是一種弱型別或者說動態語言其資料型別可以分為兩類:

  • 基本型別 Undefined,Null,Boolean,Number,String。
  • 引用型別 Object,Array,Function,Date等。

在做變數宣告時,不同的型別記憶體分配也不一樣:

  • 基礎型別儲存在棧中的簡單資料段,直接就是變數訪問的位置
  • 引用型別則儲存在堆中,而變數訪問的則是一個指標即訪問堆中物件的一個地址

所以在複製變數的時候也就會不同:

  • 基本型別: 將其副本賦值給新變數,此後便相對獨立
  • 引用型別:只是把記憶體地址副本賦值給了新變數,而指向了相同的物件,他們中任何一個做出改變都會反應在另一個身上

其實紅寶書的那句話還有後文:

基本型別的傳遞如同基本型別的複製一樣,而引用型別值的傳遞,如同引用型別變數的複製一樣。

下面我們再來看看,這兩者的不同帶來的引數傳遞的問題:

  • 基本型別: 只是把變數裡的值傳遞給引數,之後引數和這個變數互不影響
  • 物件型別: 傳遞的是物件的記憶體地址,但是它們指向的還是同一個物件,當內部函式通過該記憶體地址改變物件時,當然也就會影響到該物件。

上面可以解釋 為什麼文章開頭的例子:num 和 objectB兩個引數對與外部的影響。但是還有一個疑問,為什麼objectA在函式內部被賦值了一個全新的物件後沒有對外部造成影響呢? 這裡就要說到JS函式對於引用型別的傳遞的求值策略來說就是按共享傳遞。因此,如果你對這個引用進行第二次賦值的時候,實際上把這份引用指向了另外一個物件,所以之後對這個物件的操作不會影響到外部的物件。

還需要多說一句

為了便於操作基本型別ECMAScript還提供了3種特殊的引用型別:BooleanNumberString 他與其他引用型別相似,但同時具有各自的基本型別相應的特殊行為我們把它叫做基本包裝型別

如:Number是和基本資料型別的數值對應的引用型別。可以建立物件和呼叫本身的方法。例子程式碼如下

var num = new Number(100);
//toFixed方法表示按照指定小數位返回字串
var t = num.toFixed(3);
console.log(t); //t的值是100.000
複製程式碼

那麼我們來看這個問題:

let num = new Number(9);

function addOne(n) {
  n.x = "xx";
  n += 1;
}

addOne(num);
console.log(num); // Number {9, x: "xx"}
複製程式碼

怎麼來解釋addOne裡面的行為呢? 就留給各位各抒己見吧。。

總結

總的來說,js這門語言有很多可以細究的地方。對於求值策略理解有助於解釋我們日常程式碼中遇到的一些疑問,規避一些反模式,提高我們的程式碼魯棒性。以上是我的一些理解望各位批評指正。

參考

                                        Javascript中的求值策略

上週末我們的專欄突破����10000粉絲啦??。本來想第一萬粉絲的時候發個紅包給他,結果很尷尬,不知道哪個是第10000粉。才發現知乎專欄的粉絲管理太不友好了,所以也開一個公眾號了,如果各位喜歡可以關注一下啦 。很可惜“前端雜貨鋪”這個名字被人用了,所以現在名字暫時就叫前端雜貨鋪的新店,微訊號fezhuanlan. 歡迎各位客官光臨!!


相關文章