变量赋值
变量赋值
按值传递
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);
}
而在 JavaScript 中,对比例子如下:
function changeStuff(a, b, c) {
a = a * 10;
b.item = "changed";
c = { item: "changed" };
}
const num = 10;
const obj1 = { item: "unchanged" };
const 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 来访问相同的变量时同样会得到变化后的值。
连续赋值
JavaScript 中是支持变量的连续赋值,即譬如:
const a=b=1;
但是在连续赋值中,会发生引用保留,可以考虑如下情景:
const a = {n:1};
a.x = a = {n:2};
alert(a.x); // --> undefined
为了解释上述问题,我们引入一个新的变量
const a = {n:1};
const 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}
Deconstruction: 解构赋值
解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。这种赋值语法极度简洁,同时还比传统的属性访问方法更为清晰。传统的访问数组前三个元素的方式为:
const first = someArray[0];
const second = someArray[1];
const third = someArray[2];
const [first, second, third] = someArray;
// === Arrays
const [a, b] = [1, 2];
console.log(a, b);
//=> 1 2
// Use from functions, only select from pattern
const foo = () => {
return [1, 2, 3];
};
const [a, b] = foo();
console.log(a, b);
// => 1 2
// Omit certain values
const [a, , b] = [1, 2, 3];
console.log(a, b);
// => 1 3
// Combine with spread/rest operator (accumulates the rest of the values)
const [a, ...b] = [1, 2, 3];
console.log(a, b);
// => 1 [ 2, 3 ]
// Fail-safe.
const [, , , a, b] = [1, 2, 3];
console.log(a, b);
// => undefined undefined
// Swap variables easily without temp
const a = 1,
b = 2;
[b, a] = [a, b];
console.log(a, b);
// => 2 1
// Advance deep arrays
const [a, [b, [c, d]]] = [1, [2, [[[3, 4], 5], 6]]];
console.log("a:", a, "b:", b, "c:", c, "d:", d);
// => a: 1 b: 2 c: [ [ 3, 4 ], 5 ] d: 6
// === Objects
const { user: x } = { user: 5 };
console.log(x);
// => 5
// Fail-safe
const { user: x } = { user2: 5 };
console.log(x);
// => undefined
// More values
const { prop: x, prop2: y } = { prop: 5, prop2: 10 };
console.log(x, y);
// => 5 10
// Short-hand syntax
const { prop, prop2 } = { prop: 5, prop2: 10 };
console.log(prop, prop2);
// => 5 10
// Equal to:
const { prop: prop, prop2: prop2 } = { prop: 5, prop2: 10 };
console.log(prop, prop2);
// => 5 10
// But this does work
const a, b;
({ a, b } = { a: 1, b: 2 });
console.log(a, b);
// => 1 2
// This due to the grammar in JS.
// Starting with { implies a block scope, not an object literal.
// () converts to an expression.
// From Harmony Wiki:
// Note that object literals cannot appear in
// statement positions, so a plain object
// destructuring assignment statement
//{ x } = y must be parenthesized either
// as ({ x } = y) or ({ x }) = y.
// Combine objects and arrays
const {
prop: x,
prop2: [, y],
} = { prop: 5, prop2: [10, 100] };
console.log(x, y);
// => 5 100
// Deep objects
const {
prop: x,
prop2: {
prop2: {
nested: [, , b],
},
},
} = { prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"] } } };
console.log(x, b);
// => Hello c
// === Combining all to make fun happen
// All well and good, can we do more? Yes!
// Using as method parameters
const foo = function ({ prop: x }) {
console.log(x);
};
foo({ invalid: 1 });
foo({ prop: 1 });
// => undefined
// => 1
// Can also use with the advanced example
const foo = function ({
prop: x,
prop2: {
prop2: { nested: b },
},
}) {
console.log(x, ...b);
};
foo({ prop: "Hello", prop2: { prop2: { nested: ["a", "b", "c"] } } });
// => Hello a b c
// In combination with other ES2015 features.
// Computed property names
const name = "fieldName";
const computedObject = { [name]: name }; // (where object is { 'fieldName': 'fieldName' })
const { [name]: nameValue } = computedObject;
console.log(nameValue);
// => fieldName
// Rest and defaults
const ajax = function ({ url = "localhost", port: p = 80 }, ...data) {
console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({ url: "someHost" }, "additional", "data", "hello");
// => Url: someHost Port: 80 Rest: [ 'additional', 'data', 'hello' ]
ajax({}, "additional", "data", "hello");
// => Url: localhost Port: 80 Rest: [ 'additional', 'data', 'hello' ]
// Ooops: Doesn't work (in traceur)
const ajax = ({ url = "localhost", port: p = 80 }, ...data) => {
console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({}, "additional", "data", "hello");
// probably due to traceur compiler
const ajax = ({ url: url = "localhost", port: p = 80 }, ...data) => {
console.log("Url:", url, "Port:", p, "Rest:", data);
};
ajax({}, "additional", "data", "hello");
// Like _.pluck
const users = [
{ user: "Name1" },
{ user: "Name2" },
{ user: "Name2" },
{ user: "Name3" },
];
const names = users.map(({ user }) => user);
console.log(names);
// => [ 'Name1', 'Name2', 'Name2', 'Name3' ]
// Advanced usage with Array Comprehension and default values
const users = [
{ user: "Name1" },
{ user: "Name2", age: 2 },
{ user: "Name2" },
{ user: "Name3", age: 4 },
];
数组与迭代器
以上是数组解构赋值的一个简单示例,其语法的一般形式为:
[ variable1, variable2, ..., variableN ] = array;
这将为 variable1 到 variableN 的变量赋予数组中相应元素项的值。如果你想在赋值的同时声明变量,可在赋值语句前加入const
、let
或const
关键字,例如:
const [ variable1, variable2, ..., variableN ] = array;
let [ variable1, variable2, ..., variableN ] = array;
const [ variable1, variable2, ..., variableN ] = array;
事实上,用变量
来描述并不恰当,因为你可以对任意深度的嵌套数组进行解构:
const [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3
此外,你可以在对应位留空来跳过被解构数组中的某些元素:
const [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"
而且你还可以通过 “不定参数” 模式捕获数组中的所有尾随元素:
const [head, ...tail] = [1, 2, 3, 4];
console.log(tail); // [2, 3, 4]
当访问空数组或越界访问数组时,对其解构与对其索引的行为一致,最终得到的结果都是:undefined
。
console.log([][0]); // undefined
const [missing] = [];
console.log(missing); // undefined
请注意,数组解构赋值的模式同样适用于任意迭代器:
function* fibs() {
const a = 0;
const b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth); // 5
对象
通过解构对象,你可以把它的每个属性与不同的变量绑定,首先指定被绑定的属性,然后紧跟一个要解构的变量。
const robotA = { name: "Bender" };
const robotB = { name: "Flexo" };
const { name: nameA } = robotA;
const { name: nameB } = robotB;
console.log(nameA); // "Bender"
console.log(nameB); // "Flexo"
当属性名与变量名一致时,可以通过一种实用的句法简写:
const { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo); // "lorem"
console.log(bar); // "ipsum"
与数组解构一样,你可以随意嵌套并进一步组合对象解构:
const complicatedObj = {
arrayProp: ["Zapp", { second: "Brannigan" }],
};
const {
arrayProp: [first, { second }],
} = complicatedObj;
console.log(first); // "Zapp"
console.log(second); // "Brannigan"
当你解构一个未定义的属性时,得到的值为undefined
:
const { missing } = {};
console.log(missing);
// undefined
请注意,当你解构对象并赋值给变量时,如果你已经声明或不打算声明这些变量(亦即赋值语句前没有let
、const
或const
关键字),你应该注意这样一个潜在的语法错误:
{ blowUp } = { blowUp: 10 };
// Syntax error 语法错误
为什么会出错?这是因为 JavaScript 语法通知解析引擎将任何以 { 开始的语句解析为一个块语句(例如,{console}
是一个合法块语句)。解决方案是将整个表达式用一对小括号包裹:
({ safe } = {});
// No errors 没有语法错误
默认值
当你要解构的属性未定义时你可以提供一个默认值:
const [missing = true] = [];
console.log(missing); // true
const { message: msg = "Something went wrong" } = {};
console.log(msg); // "Something went wrong"
const { x = 3 } = {};
console.log(x); // 3
由于解构中允许对对象进行解构,并且还支持默认值,那么完全可以将解构应用在函数参数以及参数的默认值中。
function removeBreakpoint({ url, line, column }) {
// ...
}
当我们构造一个提供配置的对象,并且需要这个对象的属性携带默认值时,解构特性就派上用场了。举个例子,jQuery 的ajax
函数使用一个配置对象作为它的第二参数,我们可以这样重写函数定义:
jQuery.ajax = function (
url,
{
async = true,
beforeSend = noop,
cache = true,
complete = noop,
crossDomain = false,
global = true, // ... 更多配置
}
) {
// ... do stuff
};
同样,解构也可以应用在函数的多重返回值中,可以类似于其他语言中的元组的特性:
function returnMultipleValues() {
return [1, 2];
}
const [foo, bar] = returnMultipleValues();
Three Dots
Rest Operator
在 JavaScript 函数调用时我们往往会使用内置的 arguments 对象来获取函数的调用参数,不过这种方式却存在着很多的不方便性。譬如 arguments 对象是 Array-Like 对象,无法直接运用数组的 .map() 或者 .forEach() 函数;并且因为 arguments 是绑定于当前函数作用域,如果我们希望在嵌套函数里使用外层函数的 arguments 对象,我们还需要创建中间变量。
function outerFunction() {
// store arguments into a separated variable
const argsOuter = arguments;
function innerFunction() {
// args is an array-like object
const even = Array.prototype.map.call(argsOuter, function (item) {
// do something with argsOuter
});
}
}
ES6 中为我们提供了 Rest Operator 来以数组形式获取函数的调用参数,Rest Operator 也可以用于在解构赋值中以数组方式获取剩余的变量:
function countArguments(...args) {
return args.length;
}
// get the number of arguments
countArguments("welcome", "to", "Earth"); // => 3
// destructure an array
let otherSeasons, autumn;
[autumn, ...otherSeasons] = cold;
otherSeasons; // => ['winter']
典型的 Rest Operator 的应用场景譬如进行不定数组的指定类型过滤:
function filter(type, ...items) {
return items.filter((item) => typeof item === type);
}
filter("boolean", true, 0, false); // => [true, false]
filter("number", false, 4, "Welcome", 7); // => [4, 7]
尽管 Arrow Function 中并没有定义 arguments 对象,但是我们仍然可以使用 Rest Operator 来获取 Arrow Function 的调用参数:
(function () {
let outerArguments = arguments;
const concat = (...items) => {
console.log(arguments === outerArguments); // => true
return items.reduce((result, item) => result + item, "");
};
concat(1, 5, "nine"); // => '15nine'
})();
Spread Operator
Spread Operator 则与 Rest Opeator 的功能正好相反,其常用于进行数组构建与解构赋值,也可以用于将某个数组转化为函数的参数列表,其基本使用方式如下:
let cold = ["autumn", "winter"];
let warm = ["spring", "summer"];
// construct an array
[...cold, ...warm]; // => ['autumn', 'winter', 'spring', 'summer']
// function arguments from an array
cold.push(...warm);
cold; // => ['autumn', 'winter', 'spring', 'summer']
我们也可以使用 Spread Operator 来简化函数调用:
class King {
constructor(name, country) {
this.name = name;
this.country = country;
}
getDescription() {
return `${this.name} leads ${this.country}`;
}
}
const details = ["Alexander the Great", "Greece"];
const Alexander = new King(...details);
Alexander.getDescription(); // => 'Alexander the Great leads Greece'
还有另外一个好处就是可以用来替换 Object.assign 来方便地从旧有的对象中创建新的对象,并且能够修改部分值;譬如:
const obj = {a:1,b:2}
const obj_new_1 = Object.assign({},obj,{a:3});
const obj_new_2 = {
...obj,
a:3
}
最后我们还需要讨论下 Spread Operator 与 Iteration Protocols,实际上 Spread Operator 也是使用的 Iteration Protocols 来进行元素遍历与结果搜集;因此我们也可以通过自定义 Iterator 的方式来控制 Spread Operator 的表现。Iterable 协议规定了对象必须包含 Symbol.iterator 方法,该方法返回某个 Iterator 对象:
interface Iterable {
[Symbol.iterator]() {
//...
return Iterator;
}
}
该 Iterator 对象从属于 Iterator Protocol,其需要提供 next 成员方法,该方法会返回某个包含 done 与 value 属性的对象:
interface Iterator {
next() {
//...
return {
value: <value>,
done: <boolean>
};
};
}
典型的 Iterable 对象就是字符串:
const str = "hi";
const iterator = str[Symbol.iterator]();
iterator.toString(); // => '[object String Iterator]'
iterator.next(); // => { value: 'h', done: false }
iterator.next(); // => { value: 'i', done: false }
iterator.next(); // => { value: undefined, done: true }
[...str]; // => ['h', 'i']
我们可以通过自定义 array-like 对象的 Symbol.iterator 属性来控制其在迭代器上的效果:
function iterator() {
const index = 0;
return {
next: () => ({
// Conform to Iterator protocol
done: index >= this.length,
value: this[index++],
}),
};
}
const arrayLike = {
0: "Cat",
1: "Bird",
length: 2,
};
// Conform to Iterable Protocol
arrayLike[Symbol.iterator] = iterator;
const array = [...arrayLike];
console.log(array); // => ['Cat', 'Bird']
arrayLike[Symbol.iterator]
为该对象创建了值为某个迭代器的属性,从而使该对象符合了 Iterable 协议;而 iterator() 又返回了包含 next 成员方法的对象,使得该对象最终具有和数组相似的行为表现。