JavaScript 基础语法

44 阅读30分钟

JavaScript 基础语法

JavaScript 语言名称是商标(Oracle 公司注册的商标),正式名称是 ECMAScript。 ES6,全称 ECMAScript 6.0,是 JavaScript 的下一个版本标准

  • JavaScript 是一种轻量级的脚本语言(script language)

    所谓“脚本语言”,指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)的“脚本”。

  • JavaScript 是一门解释型语言,因为编译过程发生在代码运行中,而非之前。

    解释型语言是指在代码执行之前不需要编译成机器码,而是通过解释器解释并逐行执行代码

  • JavaScript 是一种嵌入式语言(embedded language)。

    它本身提供的核心语法不算很多,只能用来做一些数学和逻辑运算。 JavaScript 本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境(host)提供, 所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API。

    已经嵌入 JavaScript 的宿主环境:
    浏览器: 最常见的环境
    Node 项目: 服务器环境

  • JavaScript 语言是一种对象模型语言。(从语法角度看)

    各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过 JavaScript 控制这些功能。 但是,JavaScript 并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。 这导致几乎任何一个问题,JavaScript 都有多种解决方法。



1 变量

1.1 变量命名

当 JavaScript 中声明变量时,需要遵循以下规则:

  • 变量名必须以字母下划线美元符号开头,不能以数字开头。
  • 变量名只能包含字母数字下划线美元符号
  • 变量名不能使用 JavaScript 保留字关键字
  • JavaScript 变量名区分大小写,即 A 和 a 是两个不同的变量。
  • 标识符(identifier)用来识别各种值的合法名称,最常见的是变量名和函数名。
  • 标识符命名规则如下:
    1. 第一个字符可以是任意 Unicode 字母、美元符号或下划线,但不能是数字。
    2. 后面的字符可以是任意 Unicode 字母、美元符号、下划线或数字 0-9。

1.1.1 例题

下面 4 个变量声明语句中,正确的是

  • A. var default
  • B. var my_house
  • C. var my dog
  • D. var 2cats
答案

答案: B

1.2 变量声明

声明赋值值改变重复声明变量提升(整个作用域都可访问)暂时性死区(初始化之前无法使用)块级作用域window 对象的属性和方法
const常量声明时必须赋值 初始化值不可改变不可无,一定要在声明后使用
let变量可声明后赋值 undefined值可改变不可无,一定要在声明后使用
var变量值可改变可以有,变量可以在声明之前使用 undefined
  • 变量的声明和赋值,是分开的两个步骤,var a = 1 实际的步骤是 var a; a = 1;

  • const:

    • 如果声明的是引用数据类型,指的是该数据的指针不能被修改,指针指向的内容可以修改
    • 如果声明的是一个对象 obj 可以通过 obj.property 来改变对应属性的值
    • const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
  • 使用 var 重新声明一个已经存在的变量,是无效的。但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。

  • var 声明的变量,通过 function 声明的函数,会自动变为 window 对象的变量,属性或方法,但 const 和 let 不会

  • 声明后未赋值 为 undifined

var oTemp;
alert(oTemp == undefined); // true

1.2.1 变量提升

  • 变量提升(hoisting):
    • JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。
console.log(b);  // undefined,当脚本开始运行的时候,b 已经存在了,变量提升
var b = "banana";

// 相当于
var b;
console.log(b);  // undefined
b = "banana";
  • 函数提升
    • JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
    • 采用function命令声明的函数提升 优先级高于 变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。
  1. function命令声明的函数 可函数提升

f(){} 被提升到头部

var a = 1;
f; // 打印出 f(){}
f();
function f() {}

// 相当于
function f() {}
var a;
a = 1;
f;
f();
  1. 函数表达式定义的变量 不可函数提升,遵循变量提升
var a = 1;
f; // undefined
f(); // TypeError
const f = function() {};

// 相当于
var a;
const f;
a = 1;
f; // undefined
f(); // TypeError
f = function() {};

1.2.2 暂时性死区

暂时性死区(temporal dead zone,简称 TDZ):如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

console.log(a);  // ReferenceError: a is not defined,声明变量 a 之前,a 不存在
let a = "apple";

1.2.3 块级作用域

块级作用域是指在代码块(用花括号括起来的代码段)内部声明的变量和函数,其作用范围仅限于该代码块内部,并不会泄露到外部作用域。

  • 在ES6之前,JavaScript只有函数作用域和全局作用域,没有块级作用域。也就是说,使用 var 关键字声明的变量存在函数作用域,而不是块级作用域。例如:
function example() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 输出 10
}

在上面的例子中,变量x在if语句块内部声明,但在外部也可以访问到它,因为它的作用域是函数作用域。

  • 而从ES6开始,引入了新的变量声明方式let和const,这两种方式声明的变量具有块级作用域。例如:
function example() {
  if (true) {
    let x = 10;
    const y = 20;
    console.log(x, y); // 输出 10 20
  }
  console.log(x); // 报错,x未定义
  console.log(y); // 报错,y未定义
}

在上面的例子中,变量x和y都是在if语句块内部声明的,因此在外部无法访问到它们,它们的作用域仅限于if语句块内部。

总结起来,使用var声明的变量具有函数作用域,而使用let和const声明的变量具有块级作用域。

在实际开发中,推荐尽量使用let和const来声明变量,以避免变量污染和意外的作用域泄露问题。

  • 外层代码块不受内层代码块的影响
// 父作用域有声明,可在子作用域里用
// 父子作用域都有声明,子作用域就用子作用域的声明
for ( let i = 0; i < 3; i++ //父作用域) {
  //子作用域
  let i = "abc";
  console.log(i);
}
// abc
// abc
// abc

1.2.4 顶层对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window

  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self

  • Node 里面,顶层对象是global,但其他环境都不支持。

  • ES5 之中,顶层对象的属性与全局变量是等价的

window.a = 1;
a; // 1

a = 2;
window.a; // 2
  • ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this关键字,但是有局限性。

  • 全局环境中,this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,ES6 模块中this返回的是undefined
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
  • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

1.2.5 例题

1.2.5.1 变量提升、作用域

执行以下程序,输出结果是什么?

for (var i = 0; i < 10; i++) {
  setTimeout(function(){
    console.log(i);
  })
}

for (let j = 0; j < 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}
答案

  • 输出十个 10
  • 输出 0123456789

这是由于 JavaScript 中的作用域和变量提升的特点导致的差异。

在第一个循环中,使用 var 声明的变量 i 具有全局作用域,在循环中的每个迭代都会共享相同的全局作用域中的 i。由于 setTimeout 函数是异步执行的,当 setTimeout 的回调函数被执行时,循环已经执行完毕,此时 i 的值已经变成了 10。因此,当回调函数被调用时,会输出十个 10。

而在第二个循环中,使用 let 声明的变量 j 具有块级作用域,每次循环都会创建一个新的 j。这意味着在每个回调函数中,都有一个对应当前迭代的 j 的副本。因此,当回调函数被调用时,会输出对应的值 0 到 9。

1.2.5.2 变量提升、函数提升

执行以下程序,输出结果是什么?

var a = 100;
function a() {
  var a = 200;
  console.log(a);
}
a();
答案

function a() {
  var a;
  a = 200;
  console.log(a);
}
var a;
a = 100;
a(); // TypeError: a is not a function

函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。变量提升后,100 赋值给 a ,a 就不是方法,所有会报错。

类比:

var a = 100;
function a() {}

console.log(typeof a); // number
a(); // TypeError: a is not a function
// 实际相当于
function a() {}
var a;
a = 10;

console.log(typeof a); // number
a(); // TypeError: a is not a function

1.2.5.3 变量提升

执行以下程序,输出结果是什么?

function f(x) {
  console.log(x);
  var x = 200;
  console.log(x);
}

f(a = 100);
console.log(a);
答案

// 相当于
function f(x) {
  var x;	 // 形参 x 和变量 x 同名,变量 x 声明被忽略
  console.log(x); // 100
  x = 200;      
  console.log(x); // 200
}

var a = 100;
f(100);
console.log(a); // 100

1.2.5.4 变量提升、函数提升

执行以下程序,输出结果是什么?

function f () {
  var x = foo();
  var foo = function foo() {
    return "foobar";
  };
  return x;
}

f();
答案

// 相当于
function f () {
  var x;
  var foo;

  x = foo();
  foo = function foo() {
    return "foobar";
  };
  return x;
}

f();

函数表达式不会被提升,只有函数声明会被提升。这是因为函数表达式是在运行时被赋值的,而不是在编译时。在编译时,JavaScript 引擎无法确定变量所引用的函数对象的类型,因此无法进行提升。例如:

console.log(foo); // undefined

var foo = function () {
  return "foobar";
};

在上面的代码中,变量foo被声明为函数表达式,因此在代码执行之前,它的值为undefined。因此,尝试在声明之前调用foo()会导致TypeError异常。如果想要在函数表达式之前调用函数,需要使用函数声明。例如:

console.log(foo()); // "foobar"

function foo() {
  return "foobar";
}

1.2.5.5 作用域

执行以下程序,输出结果是什么?

function checkAge(age) {
  if (age < 18) {
    const message = "Sorry, you're too young.";
  } else {
    const message = "Yay! You're old enough!";
  }

  return message;
}

console.log(checkAge(21));
  • A: "Sorry, you're too young."
  • B: "Yay! You're old enough!"
  • C: ReferenceError
  • D: undefined
答案

答案: C

constlet声明的变量是具有块级作用域的,块是大括号({})之间的任何东西,即上述情况if / else语句的花括号。 由于块级作用域,我们无法在声明的块之外引用变量,因此抛出ReferenceError

1.3 变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。它能够让我们在一行简洁的代码中,同时声明和赋值多个变量。这种语法使得对数组和对象的操作更加方便和灵活。

1. 数组解构赋值

const [a, b, c] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

const [a = 1, b = 2] = [3];
console.log(a); // 3
console.log(b); // 2

const [a = 1, b = 2] = [3, undefined];
console.log(a); // 3
console.log(b); // 2

const [a, b, ...rest] = [1, 2, 3, 4, 5];
console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]

const [a, b, [c, d]] = [1, 2, [3, 4]];
console.log(a); // 1
console.log(b); // 2
console.log(c); //
console.log(d); // 4

字符串每个字符将被解构为一个数组元素,并赋值给对应的变量

const [a, b, c, d, e] = "hello";

console.log(a); // "h"
console.log(b); // "e"
console.log(c); // "l"
console.log(d); // "l"
console.log(e); // "o"

2. 对象解构赋值

const { x, y } = { x: 1, y: 2 };
console.log(x); // 1
console.log(y); // 2

const { x = 1, y = 2 } = { x: 3 };
console.log(x); // 3
console.log(y); // 2

const { x = 1, y = 2 } = { x: 3, y: undefined };
console.log(x); // 3
console.log(y); // 2

const { x: s } = { x: 1 };
console.log(s); // 1

const { x: s } = { y: 2 };
console.log(s); // undefined

const { x, ...rest } = { x: 1, y: 2, z: 3 };
console.log(x); // 1
console.log(rest); // { y: 2, z: 3 }

const {
  x,
  y,
  nested: { z },
} = { x: 1, y: 2, nested: { z: 3 } };
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3

3. 函数参数解构

// 数组
function foo([a, b]) {
  console.log(a); // 1
  console.log(b); // 2
}

let arr = [1, 2];
foo(arr);
// 对象
function foo({ x, y }) {
  console.log(x); // 1
  console.log(y); // 2
}

const obj = { x: 1, y: 2 };
foo(obj);

2 语法

2.1 条件语句

  1. if...else 结构
if (condition1) {
  // 在条件1为真时执行的代码
} else if (condition2) {
  // 在条件1为假但条件2为真时执行的代码
} else {
  // 在条件1和条件2都为假时执行的代码
}
  1. switch 结构:多个 if...else 连在一起使用的时候,可以转为使用更方便的 switch 结构。

如果所有 case 都不符合,则执行最后的 default 部分。需要注意的是,每个 case 代码块内部的 break 语句不能少,否则会接下去执行下一个 case 代码块,而不是跳出 switch 结构。

switch (x) {
  case 1:
    console.log("x 等于1");
    break;
  case 2:
    console.log("x 等于2");
    break;
  default:
    console.log("x 等于其他值");
}

2.2 循环语句

  1. for循环:
for (let i = 0; i < 5; i++) {
  console.log(i);
}
  1. while循环:
let i = 0;
while (i < 5) {
  console.log(i);
  i++;
}
  1. do...while循环:
let i = 0;
do {
  console.log(i);
  i++;
} while (i < 5); // while语句后面的分号注意不要省略。

2.2.1 跳出循环

在 JavaScript 中,我们可以使用一些关键字和语句来跳出循环。

  • continue 语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环
  • break 语句用于跳出代码块或循环。
  1. break语句:

break语句用于跳出当前的循环,并继续执行循环之后的代码。它可以用在forwhiledo...while等循环语句中。

for (let i = 0; i < 5; i++) {
  if (i === 3) {
    break; // 当i等于3时跳出循环
  }
  console.log(i);
}
  1. continue语句:

continue语句用于跳过当前循环中的其余代码,并继续下一次循环。它可以用在forwhiledo...while等循环语句中。

for (let i = 0; i < 5; i++) {
  if (i === 3) {
    continue; // 当i等于3时跳过当前循环的剩余代码,继续下一次循环
  }
  console.log(i);
}

2.3 标签 label

在 JavaScript 中,"标签"(Label)通常指的是在代码中给语句起一个标识符以便后续引用。标签可以用在循环语句和分支语句等地方,以便在复杂逻辑中进行跳转或者优化。标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。

下面是一些关于标签的常见用法:

  1. 在循环语句中使用标签:
outerLoop: for (let i = 0; i < 5; i++) {
  innerLoop: for (let j = 0; j < 5; j++) {
    if (j === 2) {
      break outerLoop; // 使用标签进行跳出循环
    }
    console.log(`i: ${i}, j: ${j}`);
  }
}

在上例中,outerLoopinnerLoop 是两个带有标签的循环语句。使用 break 语句结合标签可以在内层循环中跳出外层循环。

  1. 在分支语句中使用标签:
let num = 0;
outerIf: if (num === 0) {
  innerIf: if (num > 10) {
    console.log("This won't be logged");
    break outerIf;
  }
  console.log("Number is 0");
}

在上例中,outerIfinnerIf 是两个带有标签的条件语句。使用 break 语句结合标签可以跳出外层条件语句。

标签在一般情况下用得不多,在某些特定的场景中能够提供一些灵活性。然而,过度使用标签可能会导致代码变得复杂、难以理解和维护。因此,需要谨慎使用标签,并且要确保它们的使用不会影响代码的可读性和可维护性。

2.4 区块

JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。

对于var命令来说,JavaScript 的区块不构成单独的作用域(scope)。

{
  var a = 1;
}

a; // 1

使用 letconst 声明的变量具有块级作用域,只在区块内部有效。

{
  let x = 10; // 块级作用域变量
  const y = 20; // 块级作用域常量

  console.log(x); // 10
  console.log(y); // 20
}

console.log(x); // ReferenceError: x is not defined
console.log(y); // ReferenceError: y is not defined

3 运算符

3.1 算术运算符

  • 加法运算符:x + y

    • 相加 1 + 1 // 2
    • 连接 'a' + 'bc' // "abc"
  • 减法运算符: x - y

  • 乘法运算符: x * y

  • 除法运算符:x / y

  • 指数运算符:x ** y

    • 指数运算符是右结合 2 ** 3 ** 2 // 相当于 2 ** (3 ** 2) // 512
  • 余数运算符:x % y 运算结果的正负号由第一个运算子的正负号决定。

    • 为了得到负数的正确余数值,可以先使用绝对值函数 Math.abs(n % 2) === 1
  • 自增运算符:++x 或者 x++

  • 自减运算符:--x 或者 x--

    • 放在变量之后,先返回变量操作前的值,再进行自增/自减操作;x++ x--
    • 放在变量之前,先进行自增/自减操作,再返回变量操作后的值。++x --x
  • 数值运算符: +x

    • 将任何值转为数值(与Number函数的作用相同)
  • 负数值运算符:-x

    • 将任何值转为数值,只不过得到的值正负相反
  • 赋值运算符:=

    • x += y // 等同于 x = x + y

    • x -= y // 等同于 x = x - y

    • x *= y // 等同于 x = x * y

    • x /= y // 等同于 x = x / y

    • x %= y // 等同于 x = x % y

    • x **= y // 等同于 x = x ** y

    • x >>= y // 等同于 x = x >> y

    • x <<= y // 等同于 x = x << y

    • x >>>= y // 等同于 x = x >>> y

    • x &= y // 等同于 x = x & y

    • x |= y // 等同于 x = x | y

    • x ^= y // 等同于 x = x ^ y

    • x **= y // 等同于 x = x ** y

  • JavaScript 是一种动态类型语言:我们不指定某些变量的类型。值可以在你不知道的情况下自动转换成另一种类型,这种类型称为隐式类型转换(implicit type coercion)。

  • 如果运算子是对象,自动调用对象的valueOftoString方法,转成原始类型的值是[object Object],再运算

  • 除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。

  • 加法运算:

    • 如果有一边为字符串,那么+表示字符串连接。
    • 如果两边都没有字符串,那么就是普通的加法运算,不是 Number 类型的会先隐式转换成数字计算。
console.log("12345" + 6); // 123456
console.log(12345 + "6"); // 123456
console.log("12345" + "6"); // 123456
console.log(12345 + 6); // 12351
类型例子
String + Number'a' + 3'a3'
Boolean + Numberfalse + 33
Null + Numbernull + 33 (因为 Number(null)为 0)
Undefined + Numberundefined + 3NaN (因为 Number(undefined)为 NaN)
Boolean + Nulltrue + null1
Boolean + Undefinedtrue + undefinedNaN

3.2 比较运算符

  • 比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件

  • 比较运算符可以比较各种类型的值,不仅仅是数值。

  • 对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);

    • 如果两个运算子之中,至少有一个不是字符串
      • 如果两个运算子都是原始类型的值,则是先转成数值再比较
      • 需要注意与NaN的比较。任何值(包括NaN本身)与NaN使用非相等运算符进行比较,返回的都是false
    • 如果运算子是对象,会转为原始类型的值
      • 对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法
  • > 大于运算符

  • < 小于运算符

  • <= 小于或等于运算符

  • >= 大于或等于运算符

3.2.1 =

  • 对于相等的比较,将两个运算子都转成数值,再比较数值的大小。

    • 相等运算符(==)比较两个值是否相等,
    • 严格相等运算符(===)比较它们是否为“同一个值”。
    • 如果两个值不是同一类型,
      • 严格相等运算符(===)直接返回false
      • 相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。
    • NaN与任何值都不相等(包括自身)。另外,正0等于负0
    • 两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。
    • undefinednull与自身严格相等。
  • == 相等运算符,先转换类型再比较,两边都转成数字

  • === 严格相等运算符,先判断数据类型

  • != 不相等运算符,先转换类型再比较

  • !== 严格不相等运算符,先判断数据类型

  • = : 赋值

  • 下面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(==),最好只使用严格相等运算符(===)。

0 == ""; // true   // 等同于 0 === Number('')
0 == "0"; // true   // 等同于 0 === Number('0')

2 == true; // false  // 等同于 2 === Number(true) `=` 1
2 == false; // false  // 等同于 2 === Number(false) `=` 0

false == "false"; // false  // 等同于 0 `=` Number(false) === Number('false') `=` NaN
false == "0"; // true   // 等同于 0 `=` Number(false) === Number('0') `=` 0

false == undefined; // false
false == null; // false
null == undefined; // true

" \t\r\n " == 0; // true
  • undefinednull只有与自身比较,或者互相比较时,才会返回true;与其他类型的值比较时,结果都为false
undefined == undefined; // true
null == null; // true
undefined == null; // true

false == null; // false
false == undefined; // false

0 == null; // false
0 == undefined; // false
[] == false // true   // 等同于 0 `=` Number([]) === Number(false) `=` 0
{} == false // false  // 等同于 NAN `=` Number({}) === Number(false) `=` 0
// 因为
String([]); // ''
Number(""); // 0
// 所以
Number([]); // 0
// 因为
String({}); // '[object Object]'
Number("[object Object]"); // NaN
// 所以
Number({}); // NaN

3.3 布尔运算符

布尔运算符用于将表达式转为布尔值,一共包含四个运算符。

  • 取反运算符:!

    • 以下六个值取反后为true,其他值都为false
      • undefinednullfalse0NaN、空字符串(''
    • 对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同,!!x 等同于 Boolean(x)
  • 且运算符:&&

    • 如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);
    • 如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。
    • x && y;: 单独一行,&& 左边的命令(命令 1)返回真后,&&右边的命令(命令 2)才能够被执行;
    • 换句话说,“如果这个命令执行成功&&那么执行这个命令”。
    • 这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写。
    • if (i) { doSomething(); } 等价于 i && doSomething();
    • 常用于判断 if (a && b) {} a,b 条件都满足了才执行 if 里面的语句
  • 或运算符:||

    • 如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;
    • 如果第一个运算子的布尔值为false,则返回第二个运算子的值。
    • x || y;: 单独一行,|| 左边的命令(命令 1)未执行成功,那么就执行||右边的命令(命令 2);
    • 换句话说,如果这个命令执行失败了||那么就执行这个命令。
    • a = a || {};: 当 a 为 null 或 undefined 时将{}赋值给 a, 赋予一个初始化空对象, 目的是为了防止 a 为 null 或未定义错误
    • 常用于取值 a = b || c b 没就取 c
  • 三元运算符:?:

    • 如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值。
"t" ? "hello" : "world"; // "hello"
0 ? "hello" : "world"; // "world"

3.3.1 Null 判断运算符

读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。

const headerText = response.settings.headerText || "Hello, world!";

通过||运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为nullundefined,默认值就会生效,但是属性的值如果为空字符串或false0,默认值也会生效。

引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。

const headerText = response.settings.headerText ?? "Hello, world!";

??本质上是逻辑运算,其他两个逻辑运算符&&||的优先级到底孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

3.3.2 逻辑赋值运算符

  • 或赋值运算符:x ||= y 等同于 x || (x = y)
  • 与赋值运算符:x &&= y 等同于 x && (x = y)
  • Null 赋值运算符:x ??= y 等同于 x ?? (x = y)

相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算

3.4 二进制位运算符

  • 二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1

  • 二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0

  • 二进制否运算符(not):符号为~,表示对一个二进制位取反。

  • 异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0

  • 左移运算符(left shift):符号为<<

  • 右移运算符(right shift):符号为>>

  • 头部补零的右移运算符(zero filled right shift):符号为>>>

  • 位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。

  • 另外,虽然在 JavaScript 内部,数值都是以 64 位浮点数的形式储存,但是做位运算的时候,是以 32 位带符号的整数进行运算的,并且返回值也是一个 32 位带符号的整数。

// 求a(10)的返回结果
function a(a) {
  a ^= (1 << 4) - 1;
  return a;
}
  • 1 << 4: << 左移
    • 1(十进制) << 4 == 1(二进制) << 4 == 10000(二进制) == 16(十进制)
  • a ^= 15: ^ 异或
    • a = a ^ 15 = 10 ^ 15
    • 10(十进制) == 1010(二进制)
    • 15(十进制) == 1111(二进制)
    • 10 ^ 15 == 0101(二进制) == 5 (十进制)
1 & 2; // 0
01 & 10; //00

3.5 其他运算符

3.5.1 void 运算符

  • void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
  • 这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。
void 0; // undefined
void 0; // undefined 建议采用后一种形式,即总是使用圆括号
// 因为`void`运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,`void 4 + 7`实际上等同于`(void 4) + 7`。
// 例子
var x = 3;
void (x = 5); //undefined
x; // 5
<!-- 点击链接后,会先执行`onclick`的代码,由于`onclick`返回`false`,所以浏览器不会跳转到 example.com。 -->
<script>
  function f() {
    console.log("Hello World");
  }
</script>
<a href="http://example.com" onclick="f(); return false;">点击</a>
<!-- `void`运算符可以取代上面的写法。 -->
<a href="javascript: void(f())">文字</a>
<!-- 下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。 -->
<a href="javascript: void(document.form.submit())">提交</a>
typeof 1; //'number'
typeof (1);//'number'
typeof (); //SyntaxError 语法错误

void 0; //undefined
void (0);//undefined
void (); //SyntaxError 语法错误   `void` 操作符需要跟一个表达式。
  • 在早期的 JavaScript 中,有时候会使用 void 0 来代替 undefined,因为有些浏览器中 undefined 可能被重新赋值导致出现错误。

void 操作符的主要作用是让表达式返回 undefined,它在某些场景下是有用的。以下是一些可能使用 void 操作符的场景:

  1. 防止页面跳转:在 HTML 中,可以使用 void 操作符来防止链接的默认行为,例如 href="javascript:void(0)" 可以防止页面跳转。
  • <a href="javascript:void(0)">Click me</a>:这个例子中,void(0) 会返回 undefined,因此当用户点击链接时不会发生任何事情。
  1. 在立即执行函数表达式中使用:有时候,我们希望一个函数表达式在声明后立即执行,但是又不希望函数的返回值被使用。这时候可以使用 void 操作符来让函数返回 undefined,例如 void function() { console.log("Hello"); }();
  • void function() { console.log("Hello"); }();:这个例子中,void 操作符调用了一个立即执行函数表达式,因为这个函数表达式没有返回值,所以 void 操作符返回了 undefined
  1. 在某些语法上下文中使用:在某些语法上下文中,需要使用一个表达式,但是又不需要这个表达式的值,例如在 for 循环中使用一个空表达式来表示循环体为空,可以使用 void 操作符来让这个表达式返回 undefined

需要注意的是,使用 void 操作符的场景非常有限,因为它的作用通常可以用其他方式来实现。

3.5.2 逗号运算符

  • 逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
  • 逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。
// 先执行逗号之前的操作,然后返回逗号后面的值。
var value = (console.log("Hi!"), true);
// Hi!

value; // true

3.5.3 链判断运算符

  • ?.: 链判断运算符,直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined
  • 在引用为空的情况下不会引起错误,返回值是 undefined。如 string1?.trim()

链判断运算符?.有三种写法。

  • obj?.prop // 对象属性是否存在
  • obj?.[expr] // 对象属性是否存在
  • func?.(...args) // 函数或对象方法是否存在
// 以下写法是禁止的,会报错。

// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

3.5.4 delete 运算符

delete是 JavaScript 中的一个运算符,用于删除对象的属性或数组中的元素。 通过 var, const 或 let 关键字声明的变量无法用 delete 操作符来删除。

delete  只能删除对象自身的属性.不能删除全局作用域、或者函数作用域中,定义的变量、和参数!

  • 在 eval 中使用 var 声明的全局变量可以被 delete 删除,
  • 使用 var 声明的全局变量或者局部变量一般是不能被 delete 删除的,

它可以用于删除对象的属性,但不能用于删除变量或函数。语法如下:

delete object.property; // 删除对象的属性
delete object[index]; // 删除数组中的元素

在删除对象属性时,如果该属性存在于对象中,则该属性将被删除。如果该属性不存在,则delete操作符不起作用,仍会返回true

const obj = { name: "Lydia", age: 21 };
delete obj.age; // { name: "Lydia" }

在删除数组元素时,如果该元素存在于数组中,则该元素将被删除,并且数组的长度将减少。如果该元素不存在,则delete操作符不起作用,但是该元素的值将被设置为undefined

const arr = [1, 2, 3];
delete arr[1]; // [1, undefined, 3]
console.log(arr.length); // 3

需要注意的是,使用delete操作符删除数组元素或对象属性时,不会修改其原型链。因此,如果要删除原型链中的对象属性,则必须使用delete操作符删除该属性的实例而不是原型链中的属性。

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

Person.prototype.sayName = function () {
  console.log(`My name is ${this.name}`);
};

const lydia = new Person("Lydia");

delete lydia.name; // true
delete lydia.sayName; // false

在上面的例子中,我们创建了一个名为Person的构造函数,它具有name属性和一个sayName()方法。我们创建了一个名为lydia的对象,并使用delete操作符删除了该对象的name属性。由于该属性存在于该对象中,因此该属性被删除,并返回true。接下来,我们尝试使用delete操作符删除sayName()方法。但是,由于该方法存在于原型链中,而不是该对象本身中,因此该方法不会被删除,并返回false

3.6 运算顺序

3.6.1 优先级

从高到低

  • 乘法运算符(*
  • 加法运算符(+
  • 条件(三元)运算符(… ? … : …

3.6.2 圆括号

圆括号(())可以用来提高运算的优先级

  • 圆括号不是运算符,而是一种语法结构
    • 故不具有求值作用,如果整个表达式都放在圆括号之中,那么不会有任何效果。
    • 函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数
  • 用法:
    • 一种是把表达式放在圆括号之中,提升运算的优先级;
    • 另一种是跟在函数的后面,作用是调用函数。

3.6.3 左结合与右结合

  • JavaScript 语言的大多数运算符是“左结合”
  • 少数运算符是“右结合”,其中最主要的是赋值运算符(=)、三元条件运算符(?:)、指数运算符(**)。

3.7 例题

3.7.1 +

如下代码输出的结果是什么:

console.log(1 + "2" + "2");
console.log(1 + +"2" + "2");
console.log("A" - "B" + "2");
console.log("A" - "B" + 2);
答案

  1. 输出 "122",因为字符串拼接优先级高于加法运算符,所以先将数字 1 转换成字符串 "1",然后将它和字符串 "2" 和字符串 "2" 进行拼接,得到 "122"。

  2. 输出 "32",因为在表达式中使用了一元加号运算符,它会将字符串 "2" 转换成数字 2。所以表达式变成了 1 + 2 + "2",先进行加法运算 1 + 2 = 3,然后将数字 3 和字符串 "2" 进行拼接,得到字符串 "32"。

  3. 输出 NaN2,因为字符串 "A" 和 "B" 不能进行减法运算,所以表达式会得到一个 NaN(Not a Number)的值。然后将 NaN 和字符串 "2" 进行拼接,得到字符串 "NaN2"。

  4. 输出 NaN,因为字符串 "A" 和 "B" 不能进行减法运算,所以表达式会得到一个 NaN(Not a Number)的值。然后将 NaN 和数字 2 进行加法运算,得到 NaN。

3.7.2 [] == 0

[] == 0 为什么等于 true

当执行 [] == 0 时,JavaScript 引擎会执行以下步骤:

  1. 将左操作数 [] 转换为原始值。因为 [] 是一个对象,所以会使用 valueOf() 方法或者 toString() 方法将其转换为原始值。由于数组没有 valueOf() 方法,因此会使用 toString() 方法将其转换为字符串 ""
  2. 将字符串 "" 转换为数字类型。由于字符串 "" 不是一个有效的数字字面量,因此会被转换为数字 0
  3. 将右操作数 0 转换为原始值。因为 0 是一个数字,所以不需要进行转换。

因此两者的值相等,因此返回 true

需要注意的是,这是一种松散相等比较,因为 JavaScript 会尝试将不同类型的值进行类型转换,以便进行比较。如果使用严格相等比较 ===,则返回 false,因为它们的类型不同。

3.7.3 obj == obj

x = { x: 1 };
y = { y: 1 };
x == y;

x == y 比较的时候不会变成 '[object Object]' == '[object Object]' 这样

JavaScript 中在比较对象时,如果两个对象的引用地址不同,则它们不相等,不会调用 toString() 方法。 只有当比较的是两个基本类型的数据(如字符串、数字等)时,才会进行类型转换。例如,'1' == 1 这个表达式中,JavaScript 会将字符串 '1' 转换成数字 1,然后再进行比较。

3.7.4 obj == obj、obj === obj

输出是什么?

function checkAge(data) {
  if (data === { age: 18 }) {
    console.log("You are an adult!");
  } else if (data == { age: 18 }) {
    console.log("You are still an adult.");
  } else {
    console.log(`Hmm.. You don't have an age I guess`);
  }
}

checkAge({ age: 18 });
  • A: You are an adult!
  • B: You are still an adult.
  • C: Hmm.. You don't have an age I guess
答案

答案: C

在测试相等性时,基本类型通过它们的值(value)进行比较,而对象通过它们的引用(reference)进行比较。JavaScript 检查对象是否具有对内存中相同位置的引用。

题目中我们正在比较的两个对象不是同一个引用:作为参数传递的对象引用的内存位置,与用于判断相等的对象所引用的内存位置并不同。

这也是 { age: 18 } === { age: 18 }{ age: 18 } == { age: 18 } 都返回 false 的原因。

3.7.5 优先级

// 问输出结果
const val = 1;

console.log("Value is " + (val != "0") ? "define" : "undefine");
答案

答案: define

加号优先级高于 三目运算。低于括号。

所以括号中无论真假 加上前边的字符串都为 TRUE 三目运算为 TRUE 是 输出 define

console.log("Value is " + (val != "0") ? "define" : "undefine");

3.7.6 ++

输出是什么?

let number = 0;
console.log(number++);
console.log(++number);
console.log(number);
  • A: 1 1 2
  • B: 1 2 2
  • C: 0 2 2
  • D: 0 1 2
答案

答案: C

一元后自增运算符 ++

  1. 返回值(返回 0
  2. 值自增(number 现在是 1

一元前自增运算符 ++

  1. 值自增(number 现在是 2
  2. 返回值(返回 2

结果是 0 2 2.

3.7.7 ++

输出是什么?

let num = 10;

const increaseNumber = () => num++;
const increasePassedNumber = (number) => number++;

const num1 = increaseNumber();
const num2 = increasePassedNumber(num1);

console.log(num1);
console.log(num2);
  • A: 10, 10
  • B: 10, 11
  • C: 11, 11
  • D: 11, 12
答案

答案: A

一元操作符 ++ 先返回 操作值,再累加 操作值。num1的值是10,因为increaseNumber函数首先返回num的值,也就是10,随后再进行 num的累加。

num210因为我们将 num1传入increasePassedNumber. number等于10num1的值。同样道理,++ 先返回 操作值,再累加 操作值。)number10,所以num2也是10.

3.7.7 delete

输出是什么?

const name = "Lydia";
age = 21;

console.log(delete name);
console.log(delete age);
  • A: false, true
  • B: "Lydia", 21
  • C: true, true
  • D: undefined, undefined
答案

答案: A

delete操作符返回一个布尔值: true指删除成功,否则返回false. 但是通过 var, constlet 关键字声明的变量无法用 delete 操作符来删除。

name变量由const关键字声明,所以删除不成功:返回 false. 而我们设定age等于21时,我们实际上添加了一个名为age的属性给全局对象。对象中的属性是可以删除的,全局对象也是如此,所以delete age返回true.

3.7.8 delete

执行完如下程序后,所有能被访问到的变量包括()

var a = 1;
b = 2;
eval("var c = 3");
delete a;
delete b;
delete c;
答案

a

在 eval 中使用 var 声明的全局变量可以被 delete 删除,所以变量 c 能删除成功, 除此之外,在其他情况下,使用 var 声明的全局变量或者局部变量一般是不能被 delete 删除的,所以变量 a 无法被删除,仍然可以访问到, 而未使用 var 声明的全局变量可以使用 delete 进行删除,所以无法访问到 b。 综上,只有变量 a 未被成功删除,可以访问得到。

3.7.8 ||

const two = null || false || "";
答案 `''`

3.7.9 优先级

输出什么?

const name = "Lydia Hallie";

console.log(!typeof name === "object");
console.log(!typeof name === "string");
  • A: false true
  • B: true false
  • C: false false
  • D: true true
答案

答案: C

typeof name 返回 "string"。字符串 "string" 是一个 truthy 的值,因此 !typeof name 返回一个布尔值 falsefalse === "object"false === "string" 都返回 false

(如果我们想检测一个值的类型,我们应该用 !== 而不是 !typeof

未完待续~