JavaScript
前言
打个比方来说在前面学习的HTML和CSS中:HTML相当于骨架,人是靠骨头支撑的,同比在网页中HTML的作用就是提供一个框架,CSS就相当于是皮肤、是肉,为HTML这个架子增添色彩进行美化,使页面更加美观。
从本篇开始,要为这个有了骨头有了皮囊的HTML注入灵魂让页面“动起来”,JavaScript为HTML和用户之间增加交互和反馈。
提示:以下是本篇文章正文内容,是我从各个渠道学习到的并总结出来的一些知识点
一、基础
变量名命名规则
在 JavaScript 中,变量名称并不能随便定义,需要遵循标识符的命名规则:
- 变量名区分大小写,允许包含数字、字母、下划线
_
、美元符号$
,但不能以数字开头,即第一个字符不能为数字。 - 变量名中不能出现汉字、空格、连字符(
-
)、点(.
)号等特殊字符。 - 变量名不能是 JavaScript 中的关键字、保留字;
- 变量命名长度应该尽可能的短,并抓住要点,尽量在变量名中体现存储信息的类型;
- 尽量避免使用没有意义的命名;
当变量名中包含多个英文单词时,推荐使用驼峰命名法(大驼峰:每个单词首字母大写,例如 FileType、DataArr;小驼峰:第一个单词首字母小写后面的单词首字母大写,例如 fileType、dataArr)。
运算符优先级
下表为JavaScript运算符优先级分类:
优先级 | 运算符 | 说明 | 结合性 |
---|---|---|---|
1 | ( ) | 圆括号 (分组) | 左到右 |
2 | . [] | 成员访问、数组索引 | 左到右 |
3 | ++ -- | 后缀递增、后缀递减 | 无 |
! | 逻辑非 (取反) | 右到左 | |
~ | 按位非 | ||
+ | 一元正号 (数值转换) | ||
- | 一元负号 | ||
typeof | 返回操作数的数据类型 | ||
void | 丢弃操作数的返回值 | ||
delete | 删除对象的属性 | ||
await | 等待 Promise 完成 | ||
4 | ** | 幂 | 右到左 |
5 | * / % | 乘法、除法、求余 | 左到右 |
6 | + - | 加法、减法 | 左到右 |
7 | << >> | 位左移、位右移 | 左到右 |
>>> | 无符号右移 | ||
8 | < <= | 小于、小于等于 | 左到右 |
> >= | 大于、大于等于 | ||
instanceof | 测试对象是否为指定构造函数的实例 | ||
in | 测试对象是否包含指定属性 | ||
9 | == | 相等 | 左到右 |
!= | 不相等 | ||
=== | 严格相等 | ||
!== | 严格不相等 | ||
10 | & | 按位与 | 左到右 |
11 | ^ | 按位异或 | 左到右 |
12 | | | 按位或 | 左到右 |
13 | && | 逻辑与 | 左到右 |
14 | || | 逻辑或 | 左到右 |
15 | ?: | 条件 (三元) 运算符 | 从右到左 |
16 | = | 赋值 | 从右到左 |
+= -= | 加/减并赋值 | ||
*= /= | 乘/除并赋值 | ||
%= | 求余并赋值 | ||
**= | 幂并赋值 | ||
<<= | 位左移并赋值 | ||
>>= | 位右移并赋值 | ||
>>>= | 无符号右移并赋值 | ||
&= | 按位与并赋值 | ||
^= | 按位异或并赋值 | ||
|= | 按位或并赋值 | ||
17 | , | 逗号 (多重表达式) | 左到右 |
注意:在表中,优先级较高的运算符会优先于优先级较低的运算符进行求值。如果优先级相同,则根据结合性来决定。结合性从左到右意味着相同优先级的运算符从左到右计算;结合性从右到左意味着相同优先级的运算符从右到左计算。有些运算符是单目的(例如一元负号),有些是双目的(例如加法)。而有些运算符是三目的(例如条件运算符)
声明变量关键字
学习声明变量的关键字 var
、let
和 const
是在 JavaScript 中进行变量声明的基础。它们有不同的作用域和特点。
-
var:
- 在 ES5 及之前的版本中使用的变量声明关键字。
var
声明的变量的作用域是它当前的执行上下文,即如果是在任何函数外面,则是全局执行上下文,如果在函数里面,则是当前函数执行上下文。换句话说,var
声明的变量的作用域只能是全局或者整个函数块的。- 若在函数外部使用
var
声明变量,则该变量为全局变量,全局范围内可见。 - 使用
var
声明的变量可以重复声明,并且不会报错。
-
let:
- 引入于 ES6 (ES2015),是现代 JavaScript 推荐的变量声明方式。
let
声明的变量存在块级作用域,意味着在任何{}
块内声明的变量只在该块内部可见,外部不可见。let
声明的变量可以修改其值,但不能重复声明。
-
const:
- 引入于 ES6 (ES2015)。
const
声明的变量也存在块级作用域,与let
类似,但声明的是常量,其值不能被重新赋值。- 一旦使用
const
声明变量并初始化赋值,就不能再修改该变量的值。 const
声明的对象或数组等引用类型变量的属性或元素是可变的,但不能重新赋值整个变量。
// 使用 var 声明
function exampleVar() {
var x = 10;
if (true) {
var x = 20; // 在同一个函数作用域内,x 被覆盖为 20
}
console.log(x); // 输出 20
}
exampleVar();
// 使用 let 声明
function exampleLet() {
let y = 10;
if (true) {
let y = 20; // 在块级作用域内,y 只在此块中有效,外部 y 不受影响
}
console.log(y); // 输出 10
}
exampleLet();
// 使用 const 声明
function exampleConst() {
const PI = 3.14; // 声明一个常量 PI
// PI = 3.14159; // 不能修改常量的值,会导致错误
console.log(PI);
}
exampleConst();
综上所述,let
和 const
是现代 JavaScript 中推荐使用的变量声明方式,var
可以避免在新项目中使用。使用 const
声明常量,使用 let
声明需要在运行时可变的变量。
变量提升与暂存死区
要解释清楚这个,就要涉及到执行上下文和变量对象。
在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会创建并进入一个新的执行环境,而这个执行环境被称之为执行上下文。因此执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。在此处先简单对变量提升和暂存死区做解释,后面在JavaScript的特性和工作原理处会再次涉及进行深度学习。
变量提升和暂存死区是 JavaScript 中与变量声明和作用域相关的两个重要概念。
-
变量提升:
在 JavaScript 中,变量和函数的声明会在代码执行之前进行"提升",即将它们的声明移动到作用域顶部。这意味着可以在声明之前使用变量或函数,但它们的赋值或执行将会在实际声明之后进行。console.log(x); // undefined,变量 x 被提升,但尚未赋值 var x = 5; console.log(x); // 5,现在 x 已经赋值为 5
注意:变量提升只适用于使用
var
声明的变量,对于使用let
和const
声明的变量则不会提升,这就引出了暂存死区的概念。 -
暂存死区:
使用let
和const
声明的变量在其所在作用域中存在一个暂存死区,指的是在变量声明之前,该变量无法被访问或使用。在暂存死区内,对变量的访问会导致 ReferenceError。示例:
console.log(y); // ReferenceError: y is not defined let y = 10;
在上述示例中,尝试在声明
y
之前访问变量y
就会触发暂存死区,导致 ReferenceError。只有在实际声明语句之后,变量y
才会被初始化并且可以正常使用。暂存死区的存在是为了解决在变量提升中可能导致的问题,强制开发者在使用变量之前进行明确的声明,从而避免潜在的错误。
综上所述,变量提升是 JavaScript 中的一个特性,但使用 let
和 const
声明的变量会在其所在作用域中创建一个暂存死区,要在实际声明之后才能使用这些变量。因此,在代码中尽量将变量声明放在使用位置的前面,以避免潜在的问题。
隐式转换和强制转换
JavaScript 中有五种基本数据类型(包括 String
、Number
、Boolean
、Symbol
、Undefined
)、三种对象类型(其中包括 Object
、Date
、Array
)和两种特殊类型(其中包括 Null
、Undefined
);5种 False values:(0
、 ''
、undefined
、 null
、 NaN
)。
隐式转换
隐式转换就是自动转换,通常发生在一些数学运算中。因为 JavaScript 是一种弱类型的语言,在一个表达式中,运算符两边的类型可以不同(比如一个字符串和一个数字相加),JavaScript 解释器会在运算之前将它们的类型进行转换
var str = "http://c.biancheng.net/";
var num = 123;
var res = str + num;
document.write(typeof res); // 输出:string
document.write(res); // 输出:http://c.biancheng.net/123
JavaScript 中,表达式中包含以下运算符时,会发生隐式类型转换:
- 算术运算符:加(+)、减(-)、乘(*)、除(/)、取模(%);
- 逻辑运算符:逻辑与(&&)、逻辑或(||)、逻辑非(!);
- 字符串运算符:+、+=。
document.write("3" - 2); // 输出:1
document.write("3" + 2); // 输出:"32"
document.write(3 + "2"); // 输出:"32"
document.write("3" * "2"); // 输出:6
document.write("10" / "2"); // 输出:5
document.write(1 + true); // 输出:2
document.write(1 + false); // 输出:1
document.write(1 + undefined); // 输出:NaN
document.write(3 + null); // 输出:3
document.write("3" + null); // 输出:"3null"
document.write(true + null); // 输出:1
document.write(true + undefined); // 输出:NaN
通过运行结果可以得出:
- 字符串加数字,数字会转换为字符串;
- 数字减字符串,字符串会转换为数字,如果字符串无法转换为数字(例如"abc"、“JavaScript”),则会转换为 NaN;
- 字符串减数字,字符串会转换为数字,如果字符串无法转换为数字,则会转换为 NaN;
- 乘、除运算时,也会先将字符串转换为数字。
比较隐式转换:在比较运算符(例如 ==
和 !=
)中,JavaScript 会将两个不同数据类型进行隐式转换后再进行比较。
let num1 = 42; // num1 是数值
let str1 = "42"; // str1 是字符串
if (num1 == str1) {
// JavaScript 会将 str1 隐式转换为数值,然后进行比较
// 结果为 true,因为 42(数值)等于 42(字符串转换为数值后)
console.log("Equal!");
}
虽然隐式转换在某些情况下很方便,但有时也可能导致意外的结果。为了避免混淆和错误,建议在代码中使用显式转换来明确指定数据类型的转换。
相等运算符
- 相等(==):等号中间不能有空格,如果相等会返回true, 否则返回false;当使用 == 来比较两个值时,如果值的类型不同,则会自动进行类型转换,将其转换为相同的类型,然后再比较。
- 不相等(!=):用来判断两个值是否不相等,如果不相等返回true,否则返回false;不相等也会对变量进行自动的转换,如果转换后相等它也会返回false;
- 全等(===):用来判断两个值是否全等,它和 相等 类似,不同的是它不会做类型转换,如果两个值的类型不同,直接返回false;
- 不全等(!==):用来判断两个值是否不全等,和不等类似,不同的是它不会做自动的类型转换,如果两个值的类型不同,直接返回true。
强制转换
在 JavaScript 中,强制类型转换主要是通过调用全局函数来实现的,例如 Number()、Boolean()、parseInt()、parseFloat() 等。
以下是几种常见的强制转换方式:
- 使用
Number()
函数将值转换为数值类型。 - 使用
String()
函数将值转换为字符串类型。 - 使用
Boolean()
函数将值转换为布尔类型。 - 使用
parseInt()
或parseFloat()
函数将字符串转换为整数或浮点数。
示例:
let numStr = "42";
let numValue = Number(numStr); // 强制转换为数值类型,numValue 为 42
let strValue = String(numValue); // 强制转换为字符串类型,strValue 为 "42"
let boolValue = Boolean(strValue); // 强制转换为布尔类型,boolValue 为 true
严格模式(use strict)
use strict
的目的是指定代码在严格条件下执行。严格模式下不能使用未声明的变量。
为什么使用严格模式:
-
消除JavaScript语法的一些不合理、不严谨之处,减少一些怪异行为;
-
消除代码运行的一些不安全之处,保证代码运行的安全;
-
提高编译器效率,增加运行速度;
-
为未来新版本的JavaScript做好铺垫。
函数声明与表达式
在 JavaScript 中,函数声明和函数表达式都用于创建函数,但它们在语法和行为上有一些区别。
-
函数声明:
函数声明是通过function
关键字后跟函数名来定义的。函数声明可以在任何地方声明,因为 JavaScript 的函数声明会在代码执行之前进行"函数提升",即函数的声明会被提升到作用域的顶部,因此可以在函数声明之前调用函数。// 函数声明 function add(a, b) { return a + b; } // 可以在函数声明之前调用函数 let result = add(2, 3); console.log(result); // 输出 5
函数声明在代码块中也有效,但会被提升到包含该块的函数或全局作用域的顶部。
-
函数表达式:
函数表达式是将函数赋值给变量或将函数作为参数传递给其他函数的一种方式。它使用function
关键字来定义函数,但没有指定函数名,而是将函数赋值给一个变量。// 函数表达式 let multiply = function(x, y) { return x * y; }; // 只能在函数表达式之后调用函数 let result = multiply(2, 3); console.log(result); // 输出 6
与函数声明不同,函数表达式不会被提升。因此,只能在函数表达式之后调用函数,否则会导致变量未定义的错误。
-
箭头函数:
在 ES6 (ES2015) 中引入了箭头函数,它是函数表达式的一种更简洁的语法形式。箭头函数使用箭头(=>
)来声明函数,省略了function
关键字,并自动将其绑定到定义时的词法作用域。// 箭头函数表达式 let divide = (a, b) => a / b; // 只能在箭头函数之后调用函数 let result = divide(6, 2); console.log(result); // 输出 3
箭头函数的特点包括更简洁的语法、继承父级作用域的
this
值等,但不能用作构造函数,并且没有自己的arguments
对象。
总结:
- 函数声明提供更灵活的函数定义,并且函数会被提升,可以在声明之前调用。
- 函数表达式需要在声明之后调用,并且变量没有被提升。
- 箭头函数是函数表达式的一种简化形式,具有更短的语法和继承父级作用域的
this
值。
this指向问题
在JavaScript中,"this"关键字是一个特殊的对象,它代表当前函数的执行上下文。"this"的值在不同的情况下可能会有不同的指向。
- 全局上下文:
当"this"在全局上下文中使用时(在任何函数外部),它指向全局对象,通常在浏览器环境下是"window"对象,在Node.js环境下是"global"对象。
console.log(this); // 在浏览器中输出:window, 在Node.js中输出:global
- 函数调用:
当"this"在函数内部使用时,它的指向取决于函数调用的方式。主要有以下几种情况:
a. 函数作为独立函数调用:
当函数作为独立函数调用时,"this"指向全局对象(在严格模式下是undefined)。
function foo() {
console.log(this);
}
foo(); // 在浏览器中输出:window, 在Node.js中输出:global (非严格模式下)
b. 函数作为对象的方法调用:
当函数作为对象的方法调用时,"this"指向调用该方法的对象。
const obj = {
name: "Flynn",
sayHello: function() {
console.log(this.name);
}
};
obj.sayHello(); // 输出:Flynn
c. 使用"call"或"apply"方法:
可以通过"call"或"apply"方法显式地设置函数内部的"this"值。
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: "Flynn" };
greet.call(person); // 输出:Hello, Flynn
greet.apply(person); // 输出:Hello, Flynn
d. 使用"bind"方法:
"bind"方法创建一个新函数,并将指定的对象作为新函数的"this"值。
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: "Flynn" };
const boundGreet = greet.bind(person);
boundGreet(); // 输出:Hello, Flynn
- 构造函数中的"this":
在构造函数中,"this"指向由该构造函数创建的新实例。
function Person(name) {
this.name = name;
}
const person1 = new Person("Flynn");
console.log(person1.name); // 输出:Flynn
- 箭头函数中的"this":
箭头函数没有自己的"this"绑定机制,它继承自外部作用域的"this"值。
const obj = {
name: "Flynn",
sayHello: function() {
// 这里的箭头函数继承了外部作用域(即obj对象)的this
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc();
}
};
obj.sayHello(); // 输出:Flynn
-
在传统的Ajax中,通常使用
XMLHttpRequest
对象来发送异步请求。在success
回调函数中,this
通常指向XMLHttpRequest
对象本身。对于现代的JavaScript框架和库(如jQuery、Axios等),它们可能会在Ajax请求中更改this
的指向。 -
传统的Ajax(使用
XMLHttpRequest
):
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://xxxxxx/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(this); // this指向XMLHttpRequest对象
console.log(xhr.responseText); // 输出响应数据
}
};
xhr.send();
在传统的XMLHttpRequest
中,this
指向XMLHttpRequest
对象本身,可以通过this.responseText
获取响应数据。
- 使用jQuery的Ajax:
$.ajax({
url: 'https://xxxxxx.com/data',
method: 'GET',
success: function(data) {
console.log(this); // 在jQuery中,this通常指向调用该函数的配置对象
console.log(data); // 输出响应数据
}
});
在jQuery中,this
通常指向调用success
函数的配置对象,而不是XMLHttpRequest
对象。为了获取响应数据,可以直接使用回调函数的参数data
。
- 使用Axios的Ajax:
axios.get('https://xxxxxx.com/data')
.then(function(response) {
console.log(this); // 在Axios中,this通常为undefined或全局对象
console.log(response.data); // 输出响应数据
});
在Axios中,由于使用了箭头函数(或者Axios内部做了上下文绑定),this
通常会变为undefined
或者全局对象(在严格模式下是undefined
,非严格模式下是全局对象)。在Axios中,可以通过response.data
来获取响应数据。
使用箭头函数可以确保回调函数中的this
保持不变,不受调用方式影响。
总结:"this"的指向在JavaScript中是一个常见的问题,它主要取决于函数的调用方式以及是否使用了箭头函数。