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

JavaScript求值表达式的特殊情况

2023-10-095.6k 阅读

隐式类型转换与求值

字符串与数字的隐式转换

在JavaScript中,当字符串和数字在表达式中混合使用时,会发生隐式类型转换。例如,当使用 + 运算符连接字符串和数字时,数字会被转换为字符串。

let num = 5;
let str = '10';
let result = num + str;
console.log(result); // 输出 '510'

这里,数字 5 被隐式转换为字符串 '5',然后与字符串 '10' 进行连接。

而在其他算术运算符(如 -*/)中,字符串会被尝试转换为数字。如果转换失败,结果将为 NaN

let num1 = 5;
let str1 = '10';
let result1 = num1 - str1;
console.log(result1); // 输出 -5

let num2 = 5;
let str2 = 'abc';
let result2 = num2 - str2;
console.log(result2); // 输出 NaN

在第一个例子中,字符串 '10' 成功转换为数字 10,从而进行减法运算。在第二个例子中,字符串 'abc' 无法转换为有效的数字,因此结果为 NaN

布尔值的隐式转换

布尔值在求值表达式中也会发生隐式转换。当布尔值与数字或字符串混合使用时,true 会被转换为 1false 会被转换为 0

let boolTrue = true;
let num3 = 5;
let result3 = boolTrue + num3;
console.log(result3); // 输出 6

let boolFalse = false;
let num4 = 5;
let result4 = boolFalse - num4;
console.log(result4); // 输出 -5

在比较运算中,布尔值同样会进行隐式转换。例如,true 与数字 1 比较时,会认为它们相等。

let bool = true;
let num5 = 1;
console.log(bool == num5); // 输出 true

这里使用了宽松相等运算符 ==,它会在比较前进行隐式类型转换。如果使用严格相等运算符 ===,则会因为类型不同而返回 false

let bool1 = true;
let num6 = 1;
console.log(bool1 === num6); // 输出 false

null 和 undefined 的隐式转换

nullundefined 在大多数求值表达式中有特殊的行为。当它们与数字进行运算时,null 会被转换为 0,而 undefined 会被转换为 NaN

let num7 = 5;
let result5 = num7 + null;
console.log(result5); // 输出 5

let num8 = 5;
let result6 = num8 + undefined;
console.log(result6); // 输出 NaN

在比较运算中,nullundefined 相互宽松相等,但与其他类型比较时,会根据特定规则进行转换。

let n = null;
let u = undefined;
console.log(n == u); // 输出 true

let num9 = 5;
console.log(n == num9); // 输出 false

严格相等运算中,nullundefined 不相等,因为它们类型不同。

let n1 = null;
let u1 = undefined;
console.log(n1 === u1); // 输出 false

短路求值

逻辑与(&&)的短路求值

逻辑与运算符 && 具有短路求值的特性。它从左到右计算操作数,如果第一个操作数为 false(或者能被转换为 false 的值,如 0''nullundefinedNaN),则不会计算第二个操作数,直接返回第一个操作数。

let a = 0;
let b = 5;
let result7 = a && b;
console.log(result7); // 输出 0

这里,因为 a 的值为 0,会被转换为 false,所以不会计算 b,直接返回 a

如果第一个操作数为 true(或者能被转换为 true 的值),则会计算第二个操作数并返回其值。

let c = 5;
let d = 10;
let result8 = c && d;
console.log(result8); // 输出 10

在这个例子中,c5,会被转换为 true,因此会计算 d 并返回 d 的值。

这种短路求值特性在一些场景下非常有用,例如在条件判断中,当第一个条件为 false 时,可以避免执行第二个条件中可能存在的复杂或有副作用的操作。

function complexOperation() {
    console.log('执行复杂操作');
    return true;
}

let condition1 = false;
let result9 = condition1 && complexOperation();
// 这里不会输出 '执行复杂操作',因为 condition1 为 false,complexOperation 不会被调用

逻辑或(||)的短路求值

逻辑或运算符 || 同样具有短路求值特性。它从左到右计算操作数,如果第一个操作数为 true(或者能被转换为 true 的值),则不会计算第二个操作数,直接返回第一个操作数。

let e = 5;
let f = 10;
let result10 = e || f;
console.log(result10); // 输出 5

这里,e5,会被转换为 true,所以不会计算 f,直接返回 e

如果第一个操作数为 false(或者能被转换为 false 的值),则会计算第二个操作数并返回其值。

let g = 0;
let h = 10;
let result11 = g || h;
console.log(result11); // 输出 10

在这个例子中,g0,会被转换为 false,因此会计算 h 并返回 h 的值。

逻辑或的短路求值特性常用于提供默认值。例如,当一个变量可能为 nullundefined 时,可以使用逻辑或来提供一个默认值。

let value = null;
let defaultValue = '默认值';
let result12 = value || defaultValue;
console.log(result12); // 输出 '默认值'

自增自减运算符的特殊情况

前置自增自减(++x, --x)

前置自增(++x)和前置自减(--x)运算符会先对变量的值进行加一或减一操作,然后返回操作后的值。

let i = 5;
let result13 = ++i;
console.log(result13); // 输出 6
console.log(i); // 输出 6

在这个例子中,i 的值先被加一,然后 ++i 的结果为加一后的值 6

同样,前置自减运算符也有类似行为。

let j = 5;
let result14 = --j;
console.log(result14); // 输出 4
console.log(j); // 输出 4

这里,j 的值先被减一,然后 --j 的结果为减一后的值 4

后置自增自减(x++, x--)

后置自增(x++)和后置自减(x--)运算符则会先返回变量原来的值,然后再对变量的值进行加一或减一操作。

let k = 5;
let result15 = k++;
console.log(result15); // 输出 5
console.log(k); // 输出 6

在这个例子中,k++ 先返回 k 原来的值 5,然后 k 的值再被加一变为 6

后置自减运算符同理。

let l = 5;
let result16 = l--;
console.log(result16); // 输出 5
console.log(l); // 输出 4

这里,l-- 先返回 l 原来的值 5,然后 l 的值再被减一变为 4

在复杂表达式中的自增自减

当自增自减运算符出现在复杂表达式中时,其位置会影响整个表达式的求值结果。

let m = 5;
let result17 = (++m) + (m++);
console.log(result17); // 输出 13
console.log(m); // 输出 7

在这个表达式中,首先 ++m 会将 m 变为 6 并返回 6。然后 m++ 先返回 6(此时 m 还是 6),之后 m 变为 7。所以整个表达式为 6 + 6 = 12

再看另一个例子:

let n = 5;
let result18 = (n++) + (++n);
console.log(result18); // 输出 12
console.log(n); // 输出 7

这里,n++ 先返回 5,然后 n 变为 6。接着 ++nn 变为 7 并返回 7。所以表达式为 5 + 7 = 12

函数调用与求值顺序

函数参数的求值顺序

在JavaScript中,函数参数的求值顺序是从左到右。这意味着在调用函数时,会先计算最左边的参数,然后依次向右计算。

function printParams(a, b, c) {
    console.log(a, b, c);
}

let num10 = 5;
let num11 = 10;
let num12 = 15;
printParams(++num10, num11++, num12 + num10);
// 输出 6 10 21
// 首先 ++num10 计算为 6,然后 num11++ 先返回 10 再自增,最后 num12 + num10 计算为 15 + 6 = 21

在这个例子中,++num10 首先被计算,将 num10 变为 6 并返回 6。接着 num11++ 先返回 10,然后 num11 自增为 11。最后 num12 + num10 计算为 15 + 6 = 21

函数调用与其他表达式的求值顺序

当函数调用与其他表达式混合时,JavaScript遵循从左到右的求值顺序,先计算所有表达式的值,再进行函数调用。

function multiply(a, b) {
    return a * b;
}

let x = 5;
let y = 10;
let result19 = multiply(++x, y--) + x * y;
console.log(result19); // 输出 60 + 50 = 110
// 首先 ++x 计算为 6,y-- 先返回 10 再自减,multiply(6, 10) 返回 60
// 然后 x 为 6,y 为 9,x * y 计算为 54

在这个例子中,先计算 ++xy--,然后调用 multiply 函数得到 60。接着计算 x * y 得到 54,最后将两个结果相加。

三元运算符的特殊情况

三元运算符的基本用法

三元运算符(condition? valueIfTrue : valueIfFalse)根据条件 condition 的真假来返回不同的值。如果 conditiontrue,返回 valueIfTrue;如果 conditionfalse,返回 valueIfFalse

let age = 18;
let canVote = age >= 18? '可以投票' : '不可以投票';
console.log(canVote); // 输出 '可以投票'

在这个例子中,因为 age >= 18true,所以返回 '可以投票'

三元运算符中的隐式类型转换

与其他表达式类似,三元运算符中的操作数也可能发生隐式类型转换。

let num13 = 5;
let result20 = num13? '数字不为 0' : '数字为 0';
console.log(result20); // 输出 '数字不为 0'

这里,num13 会被转换为布尔值,因为 5 会被转换为 true,所以返回 '数字不为 0'

嵌套的三元运算符

三元运算符可以嵌套使用,但嵌套过多会使代码可读性变差。

let score = 85;
let grade = score >= 90? 'A' : score >= 80? 'B' : score >= 70? 'C' : 'D';
console.log(grade); // 输出 'B'

在这个例子中,首先判断 score >= 90,不满足。然后判断 score >= 80,满足,所以返回 'B'

数组与对象在求值表达式中的情况

数组的求值

数组在某些求值表达式中有特殊行为。例如,当数组使用 + 运算符与其他值进行运算时,数组会被转换为字符串。数组会先调用 join(',') 方法将自身转换为字符串,然后再进行运算。

let arr = [1, 2, 3];
let num14 = 5;
let result21 = arr + num14;
console.log(result21); // 输出 '1,2,35'

这里,数组 arr 被转换为字符串 '1,2,3',然后与数字 5 进行连接。

在比较运算中,数组与其他类型的比较遵循特定规则。例如,数组与数字比较时,数组会先尝试转换为数字,通常会得到 NaN

let arr1 = [1, 2, 3];
let num15 = 5;
console.log(arr1 == num15); // 输出 false

这里,数组 arr1 转换为数字得到 NaN,与 5 比较结果为 false

对象的求值

对象在求值表达式中的行为也较为特殊。当对象使用 + 运算符时,会先调用 toString() 方法将自身转换为字符串。

let obj = { name: 'John' };
let num16 = 5;
let result22 = obj + num16;
console.log(result22); // 输出 '[object Object]5'

这里,对象 obj 调用 toString() 方法返回 '[object Object]',然后与数字 5 进行连接。

在比较运算中,对象与其他类型比较通常会返回 false,除非使用自定义的比较逻辑。

let obj1 = { value: 10 };
let num17 = 10;
console.log(obj1 == num17); // 输出 false

这里,对象 obj1 与数字 10 比较,因为类型不同且没有默认的转换规则使其相等,所以返回 false

原型链与求值

原型链在属性访问求值中的作用

在JavaScript中,当访问对象的属性时,如果对象本身没有该属性,会沿着原型链向上查找。这种查找过程涉及到求值。

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(this.name +'发出声音');
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // 输出 'Buddy 发出声音'

在这个例子中,myDog 对象本身没有 speak 方法,但通过原型链,它可以找到 Animal 原型上的 speak 方法并调用。在调用 myDog.speak() 时,会沿着原型链进行求值,找到对应的方法并执行。

原型链对表达式求值的影响

当表达式涉及到对象的属性访问且属性在原型链上时,原型链的结构会影响求值结果。例如,当修改原型链上的属性值时,会影响到所有继承该属性的对象。

function Person(name) {
    this.name = name;
}

Person.prototype.age = 30;

let person1 = new Person('Alice');
let person2 = new Person('Bob');

console.log(person1.age); // 输出 30
console.log(person2.age); // 输出 30

Person.prototype.age = 35;

console.log(person1.age); // 输出 35
console.log(person2.age); // 输出 35

在这个例子中,person1person2 本身都没有 age 属性,它们通过原型链访问到 Person 原型上的 age 属性。当修改 Person 原型上的 age 属性值时,person1person2age 属性求值结果也随之改变。

严格模式下的求值变化

禁止未声明变量的使用

在严格模式下,禁止使用未声明的变量。如果在表达式中使用未声明的变量,会抛出 ReferenceError

// 'use strict';
let result23 = unDeclaredVariable; // 在严格模式下会抛出 ReferenceError

在非严格模式下,使用未声明的变量会创建一个全局变量,但在严格模式下不允许这样做,这改变了表达式的求值行为。

对函数参数的严格检查

严格模式对函数参数有更严格的检查。例如,不允许重复的函数参数名。

// 'use strict';
function duplicateParams(a, a) {
    return a + a;
}
// 在严格模式下会抛出 SyntaxError,因为有重复的参数名 a

这种严格检查会影响函数调用时表达式的求值,确保参数的正确性。

禁止八进制字面量

在严格模式下,禁止使用八进制字面量。如果在表达式中使用八进制字面量,会抛出 SyntaxError

// 'use strict';
let num18 = 0755; // 在严格模式下会抛出 SyntaxError

在非严格模式下,0755 会被解释为八进制数,但在严格模式下不允许这种写法,从而改变了相关表达式的求值。

异步操作与求值

回调函数中的求值

在JavaScript的异步编程中,回调函数是常用的方式。在回调函数中,求值会在异步操作完成后进行。

function asyncOperation(callback) {
    setTimeout(() => {
        let result24 = 5 + 10;
        callback(result24);
    }, 1000);
}

asyncOperation((result) => {
    console.log(result); // 输出 15,在 1 秒后输出
});

在这个例子中,asyncOperation 函数中的 setTimeout 模拟一个异步操作。1秒后,5 + 10 的求值结果 15 会作为参数传递给回调函数并输出。

Promise 中的求值

Promise 是另一种异步编程方式。Promise 的 then 方法中的求值会在 Promise 被解决(resolved)或被拒绝(rejected)后进行。

function asyncCalculation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let result25 = 5 * 10;
            resolve(result25);
        }, 1000);
    });
}

asyncCalculation().then((result) => {
    console.log(result); // 输出 50,在 1 秒后输出
});

这里,asyncCalculation 返回一个 Promise,1秒后 Promise 被解决,5 * 10 的求值结果 50 会传递给 then 方法中的回调函数并输出。

async/await 中的求值

async/await 是基于 Promise 的更简洁的异步编程语法。await 关键字会暂停 async 函数的执行,直到 Promise 被解决,然后返回 Promise 的值进行求值。

async function asyncFunction() {
    let promiseResult = await asyncCalculation();
    let finalResult = promiseResult + 5;
    console.log(finalResult); // 输出 55,在 1 秒后输出
}

asyncFunction();

在这个例子中,await asyncCalculation() 会等待 asyncCalculation 返回的 Promise 被解决,得到 50。然后 50 + 5 进行求值,最后输出 55

错误处理与求值

try - catch 块中的求值

在JavaScript中,try - catch 块用于捕获和处理异常。在 try 块中的表达式求值过程中,如果发生错误,会跳转到 catch 块。

try {
    let result26 = 10 / 0; // 会抛出 Infinity 的错误
    console.log(result26);
} catch (error) {
    console.log('发生错误:', error);
}

在这个例子中,10 / 0 会导致一个错误,程序会立即跳转到 catch 块,console.log(result26) 不会被执行。

错误对表达式求值的影响

当表达式求值过程中发生错误时,后续依赖该表达式结果的求值可能会受到影响。例如,在链式调用中,如果某个方法调用抛出错误,后续的方法调用将不会执行。

let obj2 = {
    value: 10,
    divideBy: function(num) {
        if (num === 0) {
            throw new Error('不能除以零');
        }
        return this.value / num;
    },
    multiplyBy: function(num) {
        return this.value * num;
    }
};

try {
    let result27 = obj2.divideBy(0).multiplyBy(2);
    console.log(result27);
} catch (error) {
    console.log('发生错误:', error);
}

在这个例子中,obj2.divideBy(0) 会抛出错误,因此 multiplyBy(2) 不会被执行,程序会进入 catch 块。

特殊值 NaN、Infinity 和 -Infinity 的求值

NaN 的求值特性

NaN 表示“非数字”,在求值表达式中有特殊行为。任何涉及 NaN 的算术运算结果仍为 NaN

let num19 = NaN;
let result28 = num19 + 5;
console.log(result28); // 输出 NaN

let result29 = NaN * 10;
console.log(result29); // 输出 NaN

NaN 与任何值(包括它自身)比较都不相等。

let num20 = NaN;
console.log(num20 == NaN); // 输出 false
console.log(num20 === NaN); // 输出 false

要判断一个值是否为 NaN,可以使用 isNaN() 函数。

let num21 = NaN;
console.log(isNaN(num21)); // 输出 true

Infinity 和 -Infinity 的求值

Infinity 表示正无穷,-Infinity 表示负无穷。当一个正数除以零会得到 Infinity,负数除以零会得到 -Infinity

let result30 = 5 / 0;
console.log(result30); // 输出 Infinity

let result31 = -5 / 0;
console.log(result31); // 输出 -Infinity

在算术运算中,Infinity-Infinity 遵循特定规则。例如,InfinityInfinity 还是 InfinityInfinity 乘以一个正数还是 Infinity 等。

let result32 = Infinity + Infinity;
console.log(result32); // 输出 Infinity

let result33 = Infinity * 5;
console.log(result33); // 输出 Infinity

Infinity 与其他值比较时,Infinity 大于任何有限数,-Infinity 小于任何有限数。

let num22 = 100;
console.log(Infinity > num22); // 输出 true
console.log(-Infinity < num22); // 输出 true