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 都有多种解决方法。
- 官方文档:MDN JavaScript
- 官方文档:MDN 重新介绍 JavaScript(JS 教程)
- JavaScript 阮一峰
- Es6 阮一峰
- 简洁归纳:1.5 万字概括 ES6 全部特性(已更新 ES2020)
- JavaScript 刷题
1 变量
1.1 变量命名
当 JavaScript 中声明变量时,需要遵循以下规则:
- 变量名必须以字母、下划线或美元符号开头,不能以数字开头。
- 变量名只能包含字母、数字、下划线或美元符号。
- 变量名不能使用 JavaScript 保留字或关键字。
- JavaScript 变量名区分大小写,即 A 和 a 是两个不同的变量。
- 标识符(identifier)用来识别各种值的合法名称,最常见的是变量名和函数名。
- 标识符命名规则如下:
- 第一个字符可以是任意 Unicode 字母、美元符号或下划线,但不能是数字。
- 后面的字符可以是任意 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
命令声明的函数提升 优先级高于 变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。
- JavaScript 引擎将函数名视同变量名,所以采用
function
命令声明的函数 可函数提升
f(){}
被提升到头部
var a = 1;
f; // 打印出 f(){}
f();
function f() {}
// 相当于
function f() {}
var a;
a = 1;
f;
f();
- 函数表达式定义的变量 不可函数提升,遵循变量提升
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,内容安全策略),那么eval
、new 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
const
和let
声明的变量是具有块级作用域的,块是大括号({}
)之间的任何东西,即上述情况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 条件语句
if...else
结构
if (condition1) {
// 在条件1为真时执行的代码
} else if (condition2) {
// 在条件1为假但条件2为真时执行的代码
} else {
// 在条件1和条件2都为假时执行的代码
}
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 循环语句
for
循环:
for (let i = 0; i < 5; i++) {
console.log(i);
}
while
循环:
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
do...while
循环:
let i = 0;
do {
console.log(i);
i++;
} while (i < 5); // while语句后面的分号注意不要省略。
2.2.1 跳出循环
在 JavaScript 中,我们可以使用一些关键字和语句来跳出循环。
continue
语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环break
语句用于跳出代码块或循环。
break
语句:
break
语句用于跳出当前的循环,并继续执行循环之后的代码。它可以用在for
、while
、do...while
等循环语句中。
for (let i = 0; i < 5; i++) {
if (i === 3) {
break; // 当i等于3时跳出循环
}
console.log(i);
}
continue
语句:
continue
语句用于跳过当前循环中的其余代码,并继续下一次循环。它可以用在for
、while
、do...while
等循环语句中。
for (let i = 0; i < 5; i++) {
if (i === 3) {
continue; // 当i等于3时跳过当前循环的剩余代码,继续下一次循环
}
console.log(i);
}
2.3 标签 label
在 JavaScript 中,"标签"(Label)通常指的是在代码中给语句起一个标识符以便后续引用。标签可以用在循环语句和分支语句等地方,以便在复杂逻辑中进行跳转或者优化。标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。
下面是一些关于标签的常见用法:
- 在循环语句中使用标签:
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}`);
}
}
在上例中,outerLoop
和 innerLoop
是两个带有标签的循环语句。使用 break
语句结合标签可以在内层循环中跳出外层循环。
- 在分支语句中使用标签:
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");
}
在上例中,outerIf
和 innerIf
是两个带有标签的条件语句。使用 break
语句结合标签可以跳出外层条件语句。
标签在一般情况下用得不多,在某些特定的场景中能够提供一些灵活性。然而,过度使用标签可能会导致代码变得复杂、难以理解和维护。因此,需要谨慎使用标签,并且要确保它们的使用不会影响代码的可读性和可维护性。
2.4 区块
JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
对于var
命令来说,JavaScript 的区块不构成单独的作用域(scope)。
{
var a = 1;
}
a; // 1
使用 let
或 const
声明的变量具有块级作用域,只在区块内部有效。
{
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)。
-
如果运算子是对象,自动调用对象的
valueOf
和toString
方法,转成原始类型的值是[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 + Number | false + 3 得 3 |
Null + Number | null + 3 得 3 (因为 Number(null)为 0) |
Undefined + Number | undefined + 3 得 NaN (因为 Number(undefined)为 NaN) |
Boolean + Null | true + null 得 1 |
Boolean + Undefined | true + undefined 得 NaN |
3.2 比较运算符
-
比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件
-
比较运算符可以比较各种类型的值,不仅仅是数值。
-
对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);
- 如果两个运算子之中,至少有一个不是字符串
- 如果两个运算子都是原始类型的值,则是先转成数值再比较
- 需要注意与
NaN
的比较。任何值(包括NaN
本身)与NaN
使用非相等运算符进行比较,返回的都是false
- 如果运算子是对象,会转为原始类型的值
- 对象转换成原始类型的值,算法是先调用
valueOf
方法;如果返回的还是对象,再接着调用toString
方法
- 对象转换成原始类型的值,算法是先调用
- 如果两个运算子之中,至少有一个不是字符串
-
>
大于运算符 -
<
小于运算符 -
<=
小于或等于运算符 -
>=
大于或等于运算符
3.2.1 =
-
对于相等的比较,将两个运算子都转成数值,再比较数值的大小。
- 相等运算符(
==
)比较两个值是否相等, - 严格相等运算符(
===
)比较它们是否为“同一个值”。 - 如果两个值不是同一类型,
- 严格相等运算符(
===
)直接返回false
, - 相等运算符(
==
)会将它们转换成同一个类型,再用严格相等运算符进行比较。
- 严格相等运算符(
NaN
与任何值都不相等(包括自身)。另外,正0
等于负0
- 两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。
undefined
和null
与自身严格相等。
- 相等运算符(
-
==
相等运算符,先转换类型再比较,两边都转成数字 -
===
严格相等运算符,先判断数据类型 -
!=
不相等运算符,先转换类型再比较 -
!==
严格不相等运算符,先判断数据类型 -
=
: 赋值 -
下面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符(
==
),最好只使用严格相等运算符(===
)。
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
undefined
和null
只有与自身比较,或者互相比较时,才会返回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
。undefined
、null
、false
、0
、NaN
、空字符串(''
)
- 对一个值连续做两次取反运算,等于将其转为对应的布尔值,与
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 判断运算符
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。
const headerText = response.settings.headerText || "Hello, world!";
通过||
运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null
或undefined
,默认值就会生效,但是属性的值如果为空字符串或false
或0
,默认值也会生效。
引入了一个新的 Null 判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。
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
操作符的场景:
- 防止页面跳转:在 HTML 中,可以使用
void
操作符来防止链接的默认行为,例如href="javascript:void(0)"
可以防止页面跳转。
<a href="javascript:void(0)">Click me</a>
:这个例子中,void(0)
会返回undefined
,因此当用户点击链接时不会发生任何事情。
- 在立即执行函数表达式中使用:有时候,我们希望一个函数表达式在声明后立即执行,但是又不希望函数的返回值被使用。这时候可以使用
void
操作符来让函数返回undefined
,例如void function() { console.log("Hello"); }();
。
void function() { console.log("Hello"); }();
:这个例子中,void
操作符调用了一个立即执行函数表达式,因为这个函数表达式没有返回值,所以void
操作符返回了undefined
。
- 在某些语法上下文中使用:在某些语法上下文中,需要使用一个表达式,但是又不需要这个表达式的值,例如在
for
循环中使用一个空表达式来表示循环体为空,可以使用void
操作符来让这个表达式返回undefined
。
需要注意的是,使用 void
操作符的场景非常有限,因为它的作用通常可以用其他方式来实现。
3.5.2 逗号运算符
- 逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
- 逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。
// 先执行逗号之前的操作,然后返回逗号后面的值。
var value = (console.log("Hi!"), true);
// Hi!
value; // true
3.5.3 链判断运算符
?.
: 链判断运算符,直接在链式调用的时候判断,左侧的对象是否为null
或undefined
。如果是的,就不再往下运算,而是返回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);
答案
-
输出 "122",因为字符串拼接优先级高于加法运算符,所以先将数字 1 转换成字符串 "1",然后将它和字符串 "2" 和字符串 "2" 进行拼接,得到 "122"。
-
输出 "32",因为在表达式中使用了一元加号运算符,它会将字符串 "2" 转换成数字 2。所以表达式变成了 1 + 2 + "2",先进行加法运算 1 + 2 = 3,然后将数字 3 和字符串 "2" 进行拼接,得到字符串 "32"。
-
输出 NaN2,因为字符串 "A" 和 "B" 不能进行减法运算,所以表达式会得到一个 NaN(Not a Number)的值。然后将 NaN 和字符串 "2" 进行拼接,得到字符串 "NaN2"。
-
输出 NaN,因为字符串 "A" 和 "B" 不能进行减法运算,所以表达式会得到一个 NaN(Not a Number)的值。然后将 NaN 和数字 2 进行加法运算,得到 NaN。
3.7.2 [] == 0
[] == 0 为什么等于 true
当执行 [] == 0
时,JavaScript 引擎会执行以下步骤:
- 将左操作数
[]
转换为原始值。因为[]
是一个对象,所以会使用valueOf()
方法或者toString()
方法将其转换为原始值。由于数组没有valueOf()
方法,因此会使用toString()
方法将其转换为字符串""
。 - 将字符串
""
转换为数字类型。由于字符串""
不是一个有效的数字字面量,因此会被转换为数字0
。 - 将右操作数
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
一元后自增运算符 ++
:
- 返回值(返回
0
) - 值自增(number 现在是
1
)
一元前自增运算符 ++
:
- 值自增(number 现在是
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
的累加。
num2
是10
因为我们将 num1
传入increasePassedNumber
. number
等于10
(num1
的值。同样道理,++
先返回 操作值,再累加 操作值。)number
是10
,所以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
, const
或 let
关键字声明的变量无法用 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
返回一个布尔值 false
。 false === "object"
和 false === "string"
都返回 false
。
(如果我们想检测一个值的类型,我们应该用 !==
而不是 !typeof
)
未完待续~