JavaScript 深入之参数按值传递

2019-12-04 02:01栏目:龙电竞官网
TAG:

JavaScript 深入之参数按值传递

2017/05/23 · JavaScript · 参数

原文出处: 冴羽   

定义ECMAScript中所有函数的参数都是按值传递的。

原文出处: 龙电竞官网,王下邀月熊   

定义

在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数:

ECMAScript中所有函数的参数都是按值传递的。

什么是按值传递呢?

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

ES6 变量声明与赋值:值传递、浅拷贝与深拷贝详解归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文首先介绍 ES6 中常用的三种变量声明方式,然后讨论了 JavaScript 按值传递的特性,最后介绍了复合类型拷贝的技巧;有兴趣的可以阅读下一章节 ES6 变量作用域与提升:变量的生命周期详解。

按值传递

举个简单的例子:

var value = 1; function foo(v) { v = 2; console.log(v); //2 } foo(value); console.log(value) // 1

1
2
3
4
5
6
7
var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

按值传递

变量声明与赋值

ES6 为我们引入了 let 与 const 两种新的变量声明关键字,同时也引入了块作用域;本文首先介绍 ES6 中常用的三种变量声明方式,然后讨论了 JavaScript 按值传递的特性以及多种的赋值方式,最后介绍了复合类型拷贝的技巧。

引用传递

拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。

所以还有另一种传递方式叫做按引用传递。

所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

举个例子:

var obj = { value: 1 }; function foo(o) { o.value = 2; console.log(o.value); //2 } foo(obj); console.log(obj.value) // 2

1
2
3
4
5
6
7
8
9
var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2

哎,不对啊,连我们的红宝书都说了 ECMAScript 中所有函数的参数都是按值传递的,这怎么能按引用传递成功呢?

而这究竟是不是引用传递呢?

var value = 1;function foo { v = 2; console.log;console.log // 1

变量声明

在 JavaScript 中,基本的变量声明可以用 var 方式;JavaScript 允许省略 var,直接对未声明的变量赋值。也就是说,var a = 1 与 a = 1,这两条语句的效果相同。但是由于这样的做法很容易不知不觉地创建全局变量(尤其是在函数内部),所以建议总是使用 var 命令声明变量。在 ES6 中,对于变量声明的方式进行了扩展,引入了 let 与 const。var 与 let 两个关键字创建变量的区别在于, var 声明的变量作用域是最近的函数块;而 let 声明的变量作用域是最近的闭合块,往往会小于函数块。另一方面,以 let 关键字创建的变量虽然同样被提升到作用域头部,但是并不能在实际声明前使用;如果强行使用则会抛出 ReferenceError 异常。

第三种传递方式

不急,让我们再看个例子:

var obj = { value: 1 }; function foo(o) { o = 2; console.log(o); //2 } foo(obj); console.log(obj.value) // 1

1
2
3
4
5
6
7
8
9
var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗?

这就要讲到其实还有第三种传递方式,叫按共享传递。

而共享传递是指,在传递对象的时候,传递对象的引用的副本。

注意: 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

最后,你可以这样理解:

参数如果是基本类型是按值传递,如果是引用类型按共享传递。

但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。

所以,高程,谁叫你是红宝书嘞!

很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。

var

var 是 JavaScript 中基础的变量声明方式之一,其基本语法为:

var x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one var y = "Hello World";

1
2
3
4
5
var x; // Declaration and initialization
x = "Hello World"; // Assignment
 
// Or all in one
var y = "Hello World";

ECMAScript 6 以前我们在 JavaScript 中并没有其他的变量声明方式,以 var 声明的变量作用于函数作用域中,如果没有相应的闭合函数作用域,那么该变量会被当做默认的全局变量进行处理。

function sayHello(){ var hello = "Hello World"; return hello; } console.log(hello);

1
2
3
4
5
function sayHello(){
var hello = "Hello World";
return hello;
}
console.log(hello);

像如上这种调用方式会抛出异常: ReferenceError: hello is not defined,因为 hello 变量只能作用于 sayHello 函数中,不过如果按照如下先声明全局变量方式再使用时,其就能够正常调用:

var hello = "Hello World"; function sayHello(){ return hello; } console.log(hello);

1
2
3
4
5
var hello = "Hello World";
function sayHello(){
return hello;
}
console.log(hello);

深入系列

JavaScript深入系列目录地址:。

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本系列:

  1. JavaScirpt 深入之从原型到原型链
  2. JavaScript 深入之词法作用域和动态作用域
  3. JavaScript 深入之执行上下文栈
  4. JavaScript 深入之变量对象
  5. JavaScript 深入之作用域链
  6. JavaScript 深入之从 ECMAScript 规范解读 this
  7. JavaScript 深入之执行上下文
  8. JavaScript 深入之闭包

    1 赞 收藏 评论

龙电竞官网 1

引用传递

let

在 ECMAScript 6 中我们可以使用 let 关键字进行变量声明:

let x; // Declaration and initialization x = "Hello World"; // Assignment // Or all in one let y = "Hello World";

1
2
3
4
5
let x; // Declaration and initialization
x = "Hello World"; // Assignment
 
// Or all in one
let y = "Hello World";

let 关键字声明的变量是属于块作用域,也就是包含在 {} 之内的作用于。使用 let 关键字的优势在于能够降低偶然的错误的概率,因为其保证了每个变量只能在最小的作用域内进行访问。

var name = "Peter"; if(name === "Peter"){ let hello = "Hello Peter"; } else { let hello = "Hi"; } console.log(hello);

1
2
3
4
5
6
7
var name = "Peter";
if(name === "Peter"){
let hello = "Hello Peter";
} else {
let hello = "Hi";
}
console.log(hello);

上述代码同样会抛出 ReferenceError: hello is not defined 异常,因为 hello 只能够在闭合的块作用域中进行访问,我们可以进行如下修改:

var name = "Peter"; if(name === "Peter"){ let hello = "Hello Peter"; console.log(hello); } else { let hello = "Hi"; console.log(hello); }

1
2
3
4
5
6
7
8
var name = "Peter";
if(name === "Peter"){
let hello = "Hello Peter";
  console.log(hello);
} else {
let hello = "Hi";
  console.log(hello);
}

我们可以利用这种块级作用域的特性来避免闭包中因为变量保留而导致的问题,譬如如下两种异步代码,使用 var 时每次循环中使用的都是相同变量;而使用 let 声明的 i 则会在每次循环时进行不同的绑定,即每次循环中闭包捕获的都是不同的 i 实例:

for(let i = 0;i < 2; i++){ setTimeout(()=>{console.log(`i:${i}`)},0); } for(var j = 0;j < 2; j++){ setTimeout(()=>{console.log(`j:${j}`)},0); } let k = 0; for(k = 0;k < 2; k++){ setTimeout(()=>{console.log(`k:${k}`)},0); } // output i:0 i:1 j:2 j:2 k:2 k:2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for(let i = 0;i < 2; i++){
        setTimeout(()=>{console.log(`i:${i}`)},0);
}
 
for(var j = 0;j < 2; j++){
        setTimeout(()=>{console.log(`j:${j}`)},0);
}
 
let k = 0;
for(k = 0;k < 2; k++){
        setTimeout(()=>{console.log(`k:${k}`)},0);
}
 
// output
i:0
i:1
j:2
j:2
k:2
k:2

拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。

const

const 关键字一般用于常量声明,用 const 关键字声明的常量需要在声明时进行初始化并且不可以再进行修改,并且 const 关键字声明的常量被限制于块级作用域中进行访问。

function f() { { let x; { // okay, block scoped name const x = "sneaky"; // error, const x = "foo"; } // error, already declared in block let x = "inner"; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
function f() {
  {
let x;
    {
      // okay, block scoped name
const x = "sneaky";
      // error, const
      x = "foo";
    }
    // error, already declared in block
let x = "inner";
  }
}

JavaScript 中 const 关键字的表现于 C 中存在着一定差异,譬如下述使用方式在 JavaScript 中就是正确的,而在 C 中则抛出异常:

# JavaScript const numbers = [1, 2, 3, 4, 6] numbers[4] = 5 console.log(numbers[4]) // print 5 # C const int numbers[] = {1, 2, 3, 4, 6}; numbers[4] = 5; // error: read-only variable is not assignable printf("%dn", numbers[4]);

1
2
3
4
5
6
7
8
9
# JavaScript
const numbers = [1, 2, 3, 4, 6]
numbers[4] = 5
console.log(numbers[4]) // print 5
 
# C
const int numbers[] = {1, 2, 3, 4, 6};
numbers[4] = 5; // error: read-only variable is not assignable
printf("%dn", numbers[4]);

从上述对比我们也可以看出,JavaScript 中 const 限制的并非值不可变性;而是创建了不可变的绑定,即对于某个值的只读引用,并且禁止了对于该引用的重赋值,即如下的代码会触发错误:

const numbers = [1, 2, 3, 4, 6] numbers = [7, 8, 9, 10, 11] // error: assignment to constant variable console.log(numbers[4])

1
2
3
const numbers = [1, 2, 3, 4, 6]
numbers = [7, 8, 9, 10, 11] // error: assignment to constant variable
console.log(numbers[4])

我们可以参考如下图片理解这种机制,每个变量标识符都会关联某个存放变量实际值的物理地址;所谓只读的变量即是该变量标识符不可以被重新赋值,而该变量指向的值还是可变的。

JavaScript 中存在着所谓的原始类型与复合类型,使用 const 声明的原始类型是值不可变的:

# Example 1 const a = 10 a = a + 1 // error: assignment to constant variable # Example 2 const isTrue = true isTrue = false // error: assignment to constant variable # Example 3 const sLower = 'hello world' const sUpper = sLower.toUpperCase() // create a new string console.log(sLower) // print hello world console.log(sUpper) // print HELLO WORLD

1
2
3
4
5
6
7
8
9
10
11
# Example 1
const a = 10
a = a + 1 // error: assignment to constant variable
# Example 2
const isTrue = true
isTrue = false // error: assignment to constant variable
# Example 3
const sLower = 'hello world'
const sUpper = sLower.toUpperCase() // create a new string
console.log(sLower) // print hello world
console.log(sUpper) // print HELLO WORLD

而如果我们希望将某个对象同样变成不可变类型,则需要使用 Object.freeze();不过该方法仅对于键值对的 Object 起作用,而无法作用于 Date、Map 与 Set 等类型:

# Example 4 const me = Object.freeze({name: “Jacopo”}) me.age = 28 console.log(me.age) // print undefined # Example 5 const arr = Object.freeze([-1, 1, 2, 3]) arr[0] = 0 console.log(arr[0]) // print -1 # Example 6 const me = Object.freeze({ name: 'Jacopo', pet: { type: 'dog', name: 'Spock' } }) me.pet.name = 'Rocky' me.pet.breed = 'German Shepherd' console.log(me.pet.name) // print Rocky console.log(me.pet.breed) // print German Shepherd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Example 4
const me = Object.freeze({name: “Jacopo”})
me.age = 28
console.log(me.age) // print undefined
# Example 5
const arr = Object.freeze([-1, 1, 2, 3])
arr[0] = 0
console.log(arr[0]) // print -1
# Example 6
const me = Object.freeze({
  name: 'Jacopo',
pet: {
    type: 'dog',
    name: 'Spock'
  }
})
me.pet.name = 'Rocky'
me.pet.breed = 'German Shepherd'
console.log(me.pet.name) // print Rocky
console.log(me.pet.breed) // print German Shepherd

即使是 Object.freeze() 也只能防止顶层属性被修改,而无法限制对于嵌套属性的修改,这一点我们会在下文的浅拷贝与深拷贝部分继续讨论。

所以还有另一种传递方式叫做按引用传递。

变量赋值

所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。

按值传递

JavaScript 中永远是按值传递(pass-by-value),只不过当我们传递的是某个对象的引用时,这里的值指的是对象的引用。按值传递中函数的形参是被调用时所传实参的副本。修改形参的值并不会影响实参。而按引用传递(pass-by-reference)时,函数的形参接收实参的隐式引用,而不再是副本。这意味着函数形参的值如果被修改,实参也会被修改。同时两者指向相同的值。我们首先看下 C 中按值传递与引用传递的区别:

void Modify(int p, int * q) { p = 27; // 按值传递 - p是实参a的副本, 只有p被修改 *q = 27; // q是b的引用,q和b都被修改 } int main() { int a = 1; int b = 1; Modify(a, &b); // a 按值传递, b 按引用传递, // a 未变化, b 改变了 return(0); }

1
2
3
4
5
6
7
8
9
10
11
12
13
void Modify(int p, int * q)
{
    p = 27; // 按值传递 - p是实参a的副本, 只有p被修改
    *q = 27; // q是b的引用,q和b都被修改
}
int main()
{
int a = 1;
int b = 1;
    Modify(a, &b);   // a 按值传递, b 按引用传递,
                     // a 未变化, b 改变了
return(0);
}

而在 JavaScript 中,对比例子如下:

function changeStuff(a, b, c) { a = a * 10; b.item = "changed"; c = {item: "changed"}; } var num = 10; var obj1 = {item: "unchanged"}; var obj2 = {item: "unchanged"}; changeStuff(num, obj1, obj2); console.log(num); console.log(obj1.item); console.log(obj2.item); // 输出结果 10 changed unchanged

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function changeStuff(a, b, c)
{
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}
 
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
 
changeStuff(num, obj1, obj2);
 
console.log(num);
console.log(obj1.item);    
console.log(obj2.item);
 
// 输出结果
10
changed
unchanged

JavaScript 按值传递就表现于在内部修改了 c 的值但是并不会影响到外部的 obj2 变量。如果我们更深入地来理解这个问题,JavaScript 对于对象的传递则是按共享传递的(pass-by-sharing,也叫按对象传递、按对象共享传递)。最早由Barbara Liskov. 在1974年的GLU语言中提出;该求值策略被用于Python、Java、Ruby、JS等多种语言。该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。按共享传递的直接表现就是上述代码中的 obj1,当我们在函数内修改了 b 指向的对象的属性值时,我们使用 obj1 来访问相同的变量时同样会得到变化后的值。

var obj = { value: 1};function foo { o.value = 2; console.log; //2}foo;console.log // 2

连续赋值

JavaScript 中是支持变量的连续赋值,即譬如:

var a=b=1;

1
var a=b=1;

但是在连续赋值中,会发生引用保留,可以考虑如下情景:

var a = {n:1}; a.x = a = {n:2}; alert(a.x); // --> undefined

1
2
3
var a = {n:1};  
a.x = a = {n:2};  
alert(a.x); // --> undefined  

为了解释上述问题,我们引入一个新的变量:

var a = {n:1}; var b = a; // 持有a,以回查 a.x = a = {n:2}; alert(a.x);// --> undefined alert(b.x);// --> [object Object]

1
2
3
4
5
var a = {n:1};  
var b = a; // 持有a,以回查  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> [object Object]  

实际上在连续赋值中,值是直接赋予给变量指向的内存地址:

a.x = a = {n:2} │ │ {n:1}<──┘ └─>{n:2}

1
2
3
a.x  =  a  = {n:2}
              │      │
      {n:1}<──┘      └─>{n:2}

版权声明:本文由龙竞技官网发布于龙电竞官网,转载请注明出处:JavaScript 深入之参数按值传递