MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript赋值操作符的边界条件

2022-12-095.1k 阅读

JavaScript 赋值操作符基础回顾

在 JavaScript 中,赋值操作符 = 是最为基础且常用的操作符之一,用于将右侧的值赋给左侧的变量。例如:

let num = 10;

这里,数字 10 被赋值给了变量 num。除了简单的 = 操作符外,还有一系列复合赋值操作符,像 +=-=*=/=%= 等。以 += 为例:

let a = 5;
a += 3; // 等同于 a = a + 3; 此时 a 的值为 8

变量声明与赋值的边界

未声明变量的赋值

在严格模式下,对未声明的变量进行赋值会抛出 ReferenceError。例如:

'use strict';
undeclaredVar = 10; 
// Uncaught ReferenceError: undeclaredVar is not defined

然而,在非严格模式下,对未声明的变量赋值会在全局作用域中隐式创建一个全局变量。

nonStrictVar = 20;
console.log(window.nonStrictVar); // 20,在浏览器环境中,全局对象为 window

这种隐式创建全局变量的行为可能会导致命名冲突和难以调试的问题,因此在现代 JavaScript 开发中,强烈建议始终使用 'use strict';

块级作用域与变量赋值

在 ES6 引入块级作用域之前,JavaScript 只有函数作用域。使用 var 声明的变量在函数内有效,但是如果在块级作用域内(如 if 块、for 循环块等)使用 var 进行赋值,变量会被提升到函数作用域顶部。

function varInBlock() {
    if (true) {
        var localVar = 'inside if';
    }
    console.log(localVar); // 'inside if',变量 localVar 被提升到函数作用域顶部
}
varInBlock();

而使用 letconst 声明的变量具有块级作用域。

function letInBlock() {
    if (true) {
        let localVar = 'inside if';
    }
    console.log(localVar); 
    // Uncaught ReferenceError: localVar is not defined,localVar 只在 if 块内有效
}
letInBlock();

当使用 const 进行赋值时,必须在声明时就进行初始化,并且一旦赋值后,不能再重新赋值(对于基本数据类型)。

const PI = 3.14159;
// PI = 3.14; 
// Uncaught TypeError: Assignment to constant variable.

但对于对象和数组,虽然不能重新赋值整个对象或数组,但可以修改其内部属性或元素。

const obj = { key: 'value' };
obj.newKey = 'new value';
console.log(obj); // { key: 'value', newKey: 'new value' }

基本数据类型赋值边界

数值类型赋值

JavaScript 中的数值类型包括整数和浮点数。在进行数值赋值时,需要注意一些边界情况。例如,JavaScript 使用 64 位双精度浮点数来表示数值,这会导致一些精度问题。

let num1 = 0.1;
let num2 = 0.2;
let sum = num1 + num2;
console.log(sum === 0.3); // false,实际结果为 0.30000000000000004

这是因为 0.1 和 0.2 在二进制中无法精确表示,导致相加后的结果有微小偏差。在进行数值比较和精确计算时,需要使用 toFixed() 方法或者专门的高精度计算库,如 big.js

let num1 = 0.1;
let num2 = 0.2;
let sum = num1 + num2;
console.log(sum.toFixed(1) === '0.3'); // true

字符串类型赋值

字符串在 JavaScript 中是不可变的。当对字符串变量进行赋值操作时,实际上是创建了一个新的字符串对象。

let str1 = 'hello';
let str2 = str1;
str1 = 'world';
console.log(str2); // 'hello',str2 不受 str1 重新赋值的影响

字符串拼接操作通常使用 + 操作符,这也涉及到赋值操作。

let part1 = 'Hello';
let part2 = 'World';
let fullStr = part1 + ', ' + part2;
console.log(fullStr); // 'Hello, World'

在处理大字符串时,使用 += 操作符可能会导致性能问题,因为每次 += 操作都会创建一个新的字符串对象。此时,可以使用 Array.join() 方法来提高性能。

let parts = [];
for (let i = 0; i < 1000; i++) {
    parts.push(i.toString());
}
let bigStr = parts.join('');

布尔类型赋值

布尔类型只有两个值:truefalse。在赋值时,需要注意一些类型转换的边界情况。例如,在条件判断中,非布尔值会被转换为布尔值。

let truthyValue = 'any non - empty string';
if (truthyValue) {
    console.log('This is truthy');
}
let falsyValue = '';
if (!falsyValue) {
    console.log('This is falsy');
}

当进行赋值操作并与条件判断结合时,要确保理解值的真实布尔含义。例如:

let flag = 1; // 1 是真值
if (flag) {
    console.log('Flag is truthy');
}

引用数据类型赋值边界

对象类型赋值

当对一个对象进行赋值时,实际上是传递了对象的引用,而不是创建一个新的对象副本。

let obj1 = { key: 'value' };
let obj2 = obj1;
obj2.newKey = 'new value';
console.log(obj1); // { key: 'value', newKey: 'new value' },因为 obj1 和 obj2 指向同一个对象

如果想要创建一个对象的副本,可以使用多种方法。一种简单的方法是使用 Object.assign() 方法。

let obj1 = { key: 'value' };
let obj2 = Object.assign({}, obj1);
obj2.newKey = 'new value';
console.log(obj1); // { key: 'value' }
console.log(obj2); // { key: 'value', newKey: 'new value' }

另一种方法是使用展开运算符(ES6)。

let obj1 = { key: 'value' };
let obj2 = {...obj1 };
obj2.newKey = 'new value';
console.log(obj1); // { key: 'value' }
console.log(obj2); // { key: 'value', newKey: 'new value' }

然而,这两种方法都是浅拷贝,对于嵌套对象,它们不会递归地复制内部对象。例如:

let nestedObj1 = { inner: { subKey:'subValue' } };
let nestedObj2 = Object.assign({}, nestedObj1);
// 或者 let nestedObj2 = {...nestedObj1 };
nestedObj2.inner.newSubKey = 'new sub value';
console.log(nestedObj1.inner); // { subKey:'subValue', newSubKey: 'new sub value' }

要进行深拷贝,可以使用 JSON.parse(JSON.stringify()),但这种方法有局限性,它不能处理函数、正则表达式等特殊对象。

let nestedObj1 = { inner: { subKey:'subValue' } };
let nestedObj2 = JSON.parse(JSON.stringify(nestedObj1));
nestedObj2.inner.newSubKey = 'new sub value';
console.log(nestedObj1.inner); // { subKey:'subValue' }

数组类型赋值

与对象类似,数组赋值也是传递引用。

let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4],因为 arr1 和 arr2 指向同一个数组

要复制数组,可以使用 slice() 方法或者展开运算符。

let arr1 = [1, 2, 3];
let arr2 = arr1.slice();
// 或者 let arr2 = [...arr1];
arr2.push(4);
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3, 4]

对于多维数组,同样需要注意浅拷贝和深拷贝的问题。例如:

let multiArr1 = [[1, 2], [3, 4]];
let multiArr2 = multiArr1.slice();
multiArr2[0][0] = 10;
console.log(multiArr1); // [[10, 2], [3, 4]],因为是浅拷贝

实现多维数组的深拷贝可以使用递归方法。

function deepCopyArray(arr) {
    return arr.map(item => Array.isArray(item)? deepCopyArray(item) : item);
}
let multiArr1 = [[1, 2], [3, 4]];
let multiArr2 = deepCopyArray(multiArr1);
multiArr2[0][0] = 10;
console.log(multiArr1); // [[1, 2], [3, 4]]

函数类型赋值

在 JavaScript 中,函数是一等公民,可以像其他数据类型一样进行赋值。

function add(a, b) {
    return a + b;
}
let addFunction = add;
console.log(addFunction(2, 3)); // 5

当对函数进行赋值时,要注意函数的作用域和闭包问题。例如:

function outer() {
    let localVar = 'outer variable';
    function inner() {
        console.log(localVar);
    }
    return inner;
}
let innerFunction = outer();
innerFunction(); // 'outer variable',因为 innerFunction 形成了闭包

赋值操作符与运算符优先级

赋值操作符的优先级

赋值操作符 = 的优先级相对较低。例如,在表达式 a = b + c 中,先计算 b + c 的值,然后再将结果赋给 a

let b = 2;
let c = 3;
let a = b + c;
console.log(a); // 5

复合赋值操作符的优先级与 = 相同。在复杂表达式中,需要注意优先级可能导致的结果与预期不符。例如:

let x = 2;
let y = 3;
let result = x += y * 2;
// 先计算 y * 2 = 6,然后 x += 6 即 x = x + 6 = 8,最后 result = 8
console.log(result); // 8

与其他运算符混合使用的边界

当赋值操作符与逻辑运算符、比较运算符等混合使用时,需要特别注意优先级。例如:

let a = true;
let b = false;
let c = a && (b = true);
// 先计算 a && b,此时 b 为 false,但是 b = true 会被执行,b 变为 true,c 为 false
console.log(b); // true
console.log(c); // false

在链式赋值中,也要注意优先级和变量的作用域。

let x, y, z;
x = y = z = 10;
// 从右向左赋值,先将 10 赋给 z,然后将 z 的值赋给 y,最后将 y 的值赋给 x
console.log(x, y, z); // 10 10 10

赋值操作符在函数参数与返回值中的边界

函数参数的赋值

当函数接受参数时,实际上是对参数进行赋值操作。对于基本数据类型,是值传递;对于引用数据类型,是引用传递。

function changeNumber(num) {
    num = num + 1;
    return num;
}
let originalNum = 5;
let newNum = changeNumber(originalNum);
console.log(originalNum); // 5,基本数据类型值传递,原变量不受影响
console.log(newNum); // 6

function changeArray(arr) {
    arr.push(4);
    return arr;
}
let originalArr = [1, 2, 3];
let newArr = changeArray(originalArr);
console.log(originalArr); // [1, 2, 3, 4],引用数据类型引用传递,原数组被修改
console.log(newArr); // [1, 2, 3, 4]

函数返回值的赋值

当函数返回一个值并进行赋值操作时,要注意返回值的类型和可能的边界情况。例如,函数可能返回 undefined,如果不加以处理,可能会导致后续代码出错。

function mightReturnUndefined() {
    let condition = false;
    if (condition) {
        return 'value';
    }
}
let result = mightReturnUndefined();
if (result!== undefined) {
    console.log(result.length); 
    // 如果不判断,result 为 undefined 时会抛出 TypeError: Cannot read property 'length' of undefined
}

对于返回对象或数组的函数,同样要注意深拷贝和浅拷贝的问题,以避免意外修改原数据。

function returnObject() {
    return { key: 'value' };
}
let obj = returnObject();
let newObj = Object.assign({}, obj);
// 或者 let newObj = {...obj };
newObj.newKey = 'new value';

赋值操作符在循环与迭代中的边界

for 循环中的赋值

for 循环中,初始化部分涉及到变量的赋值。

for (let i = 0; i < 10; i++) {
    console.log(i);
}
// 这里 i 在每次循环开始时被赋值,并且具有块级作用域

如果在 for 循环中使用 var 声明变量,会有变量提升和作用域的问题。

for (var j = 0; j < 10; j++) {
    console.log(j);
}
console.log(j); // 10,j 被提升到外部作用域

whiledo - while 循环中的赋值

whiledo - while 循环中,也会涉及到变量的赋值操作,用于控制循环的条件。

let count = 0;
while (count < 5) {
    console.log(count);
    count++;
}
let num = 0;
do {
    console.log(num);
    num++;
} while (num < 5);

在这些循环中,要注意变量的初始赋值和每次迭代中的赋值操作,以确保循环按预期执行,避免无限循环或错误的迭代次数。

数组与对象迭代中的赋值

当使用 for...offor...in 对数组或对象进行迭代时,可能会涉及到赋值操作。例如,在 for...of 循环中对数组元素进行赋值。

let arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i] * 2;
}
console.log(arr); // [2, 4, 6]

for...in 循环中对对象属性进行赋值时,要注意可能会遍历到原型链上的属性。

let obj = { key1: 'value1', key2: 'value2' };
for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
        obj[prop] = obj[prop].toUpperCase();
    }
}
console.log(obj); // { key1: 'VALUE1', key2: 'VALUE2' }

赋值操作符在异常处理中的边界

try - catch 块中的赋值

try - catch 块中,变量的赋值行为可能会受到异常的影响。例如:

try {
    let successVar = 'Success value';
    throw new Error('An error occurred');
    console.log(successVar); 
    // 这行代码不会执行,因为在抛出异常后,try 块中后续代码不再执行
} catch (error) {
    let errorVar = 'Error handling value';
    console.log(errorVar); // 'Error handling value',errorVar 只在 catch 块内有效
}
// console.log(errorVar); 
// Uncaught ReferenceError: errorVar is not defined,errorVar 超出作用域

异常对赋值操作的影响

如果在赋值操作过程中发生异常,赋值可能不会完成。例如:

function divide(a, b) {
    if (b === 0) {
        throw new Error('Cannot divide by zero');
    }
    return a / b;
}
let result;
try {
    result = divide(10, 0);
} catch (error) {
    console.log('Error:', error.message);
}
console.log(result); // undefined,因为赋值未完成

在编写代码时,要考虑到异常情况下赋值操作的完整性,以及如何正确处理异常以确保程序的健壮性。

赋值操作符在模块化与作用域链中的边界

模块内的赋值与作用域

在 ES6 模块中,每个模块都有自己的作用域。变量的赋值在模块内是局部的,除非通过 export 导出。

// module.js
let moduleVar = 'Module value';
function moduleFunction() {
    return moduleVar;
}
export { moduleFunction };
// main.js
import { moduleFunction } from './module.js';
console.log(moduleFunction()); // 'Module value'
// console.log(moduleVar); 
// Uncaught ReferenceError: moduleVar is not defined,moduleVar 未导出,在外部不可访问

作用域链与赋值

当在一个函数中进行赋值操作时,JavaScript 会沿着作用域链查找变量。如果在当前作用域中找不到变量,会向上一级作用域查找。

let outerVar = 'Outer value';
function innerFunction() {
    let innerVar = 'Inner value';
    function nestedFunction() {
        let nestedVar = 'Nested value';
        console.log(outerVar); // 'Outer value',通过作用域链找到外部变量
        console.log(innerVar); // 'Inner value',找到上一级作用域变量
        console.log(nestedVar); // 'Nested value'
    }
    nestedFunction();
}
innerFunction();

在进行赋值操作时,要注意作用域链的查找规则,避免意外覆盖或访问不到预期的变量。

赋值操作符在性能方面的边界

频繁赋值的性能影响

频繁的赋值操作,尤其是在循环中对基本数据类型进行赋值,可能会影响性能。例如:

for (let i = 0; i < 1000000; i++) {
    let temp = i * 2;
    // 这里每次循环都创建一个新的临时变量 temp
}

在这种情况下,可以提前声明变量并复用,以减少内存分配和垃圾回收的开销。

let temp;
for (let i = 0; i < 1000000; i++) {
    temp = i * 2;
}

引用类型赋值的性能

对于引用类型,如对象和数组,浅拷贝和深拷贝操作涉及到赋值,性能开销较大。尤其是深拷贝,特别是对于大型嵌套对象或数组,递归深拷贝可能会导致栈溢出。在选择深拷贝方法时,要根据数据结构的复杂程度和性能需求进行权衡。例如,JSON.parse(JSON.stringify()) 虽然简单,但对于包含函数、正则等特殊对象的结构不适用,并且性能也不一定是最优的。在需要高性能的场景下,可以考虑使用更高效的深拷贝库。

通过深入理解 JavaScript 赋值操作符的这些边界条件,开发者能够编写出更健壮、高效且易于维护的代码。在实际开发中,时刻注意这些边界情况,能够避免许多潜在的错误和性能问题。