基础语法与数据类型
约 3176 字大约 11 分钟
2025-11-24
变量声明
思考
变量声明的方式与它们的区别?
在 JavaScript 中,有多种变量声明方式,主要包括使用 var、let、const 关键字,下面详细介绍它们及其区别。
1. var
- 特点
- 函数作用域:
var声明的变量为函数作用域,意味着在函数内部任何位置声明的var变量,在整个函数内部都是可见的。 - 变量提升:使用
var声明的变量会被提升到当前作用域的顶部,可以在声明之前访问,但值为undefined。 - 可重复声明:在同一作用域内可以多次使用
var声明同一个变量。
- 函数作用域:
- 示例代码
function testVar() {
console.log(num); // 输出: undefined
var num = 10;
console.log(num); // 输出: 10
var num = 20; // 可以重复声明
console.log(num); // 输出: 20
}
testVar();想一想,下面的代码输出什么?为什么?
JS code example
2. let
- 特点
- 块级作用域:
let声明的变量具有块级作用域,变量只在声明它的块(如if语句、for循环、while循环等)内部可见。 - 不存在变量提升:
let声明的变量不会被提升到当前作用域的顶部,在声明之前访问会导致ReferenceError。 - 不可重复声明:在同一作用域内不能使用
let重复声明同一个变量。
- 块级作用域:
- 示例代码
function testLet() {
// console.log(count); // 报错: ReferenceError: Cannot access 'count' before initialization
let count = 10;
console.log(count); // 输出: 10
// let count = 20; // 报错: SyntaxError: Identifier 'count' has already been declared
if (true) {
let innerCount = 20;
console.log(innerCount); // 输出: 20
}
// console.log(innerCount); // 报错: ReferenceError: innerCount is not defined
}
testLet();3. const
- 特点
- 块级作用域:和
let一样,const声明的常量具有块级作用域。 - 不存在变量提升:
const声明的常量不会被提升到当前作用域的顶部,在声明之前访问会导致ReferenceError。 - 必须初始化:使用
const声明常量时,必须同时进行初始化,否则会报错。 - 常量值不可变(基本类型):一旦声明并初始化,基本类型的常量的值就不能再被重新赋值,但如果常量是引用类型(如对象、数组),可以修改其内部属性。
- 不可重复声明:在同一作用域内不能使用
const重复声明同一个常量。
- 块级作用域:和
- 示例代码
function testConst() {
// const PI; // 报错: SyntaxError: Missing initializer in const declaration
const PI = 3.14;
// PI = 3.14159; // 报错: TypeError: Assignment to constant variable.
console.log(PI); // 输出: 3.14
const person = { name: "Alice" };
person.name = "Bob"; // 可以修改对象的属性
console.log(person.name); // 输出: Bob
}
testConst();总结
| 声明方式 | 作用域 | 提升情况 | 可重复声明 | 初始化要求 | 值可变性 |
|---|---|---|---|---|---|
var | 函数作用域 | 变量提升 | 是 | 否 | 是 |
let | 块级作用域 | 无提升 | 否 | 否 | 是 |
const | 块级作用域 | 无提升 | 否 | 是 | 基本类型不可变,引用类型可修改内部属性 |
在实际开发中,建议优先使用 const 声明常量,当需要重新赋值时使用 let,尽量避免使用 var 以减少潜在的问题。
问题补充
想一想,下面的代码输出什么?
JS code example
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
}
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
}var声明的变量具有函数作用域,且在循环中共享同一个闭包变量i。当setTimeout回调执行时,循环早已结束,i的值为5。let声明的变量具有块级作用域,每次循环迭代都会创建一个新的词法环境,每个setTimeout回调捕获的是各自迭代中的i值。
for-in 与 for-of
思考
for...in 与 for...of 的区别?
for...in 与 for...of 是 JavaScript 中两种不同的遍历语法,用途、遍历目标和行为完全不同。
1. for...in
- 遍历对象的可枚举属性名(key/索引),包括继承的可枚举属性。
- 主要用于 对象(Object),但也可用于数组(不推荐)。
const obj = { a: 1, b: 2 };
for (const key in obj) {
console.log(key, obj[key]); // 'a' 1, 'b' 2
}
const arr = ["x", "y"];
for (const index in arr) {
console.log(index, arr[index]); // '0' 'x', '1' 'y' (输出字符串索引!)
}问题:
- 遍历数组时,
index是 字符串(如'0'),不是数字。
for...of
- 遍历可迭代对象的值(value),如数组、字符串、Map、Set、NodeList 等。
const arr = ["x", "y"];
for (const value of arr) {
console.log(value); // 'x', 'y'
}
const str = "hi";
for (const char of str) {
console.log(char); // 'h', 'i'
}
const map = new Map([
[1, "a"],
[2, "b"],
]);
for (const [k, v] of map) {
console.log(k, v); // 1 'a', 2 'b'
}优点:
- 直接拿到值,无需通过索引访问。
- 支持
break、continue、return。 - 顺序可靠(按迭代器协议定义的顺序)。
数据解构
思考
如何进行数据解构?
解构的本质是按结构取值,是从原对象/数组中读取值赋值给新变量。解构时若读取到的是原始值,则赋值的是值的副本;但若读取到的是引用值时,那么赋值的是值的引用。
解构对象
对象根据 属性名 进行解构。
直接解构
const obj = { name: "Alice", age: 25 };
// 解构:变量名必须与属性名一致
const { name, age } = obj;
console.log(name); // 'Alice'
console.log(age); // 25重命名变量
如果想用不同名字接收属性值:
const { name: userName, age: userAge } = obj;
console.log(userName); // 'Alice'设置默认值
当属性不存在时提供默认值,存在则保持原值。
const { age = 18, gender = "unknown" } = obj;
console.log(gender); // 'unknown',因为 obj 没有 gender,使用默认值
console.log(age); // 25,因为 obj 有 age,使用原值解构嵌套对象
const user = {
id: 1,
profile: {
email: "[email protected]",
settings: { theme: "dark" },
},
};
const {
profile: { email },
profile: {
settings: { theme },
},
} = user;
// 或更简洁:
const {
profile: {
email,
settings: { theme },
},
} = user;
console.log(email); // '[email protected]'
console.log(theme); // 'dark'剩余属性
const { name, ...rest } = { name: "Bob", age: 30, city: "Beijing" };
console.log(name); // 'Bob'
console.log(rest); // { age: 30, city: 'Beijing' }在函数参数中解构
function greet({ name, age }) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet({ name: "Charlie", age: 40 }); // Hello, Charlie! You are 40 years old.解构数组
数组中按 位置(索引) 解构数据。
基础用法
const arr = [10, 20, 30];
const [a, b, c] = arr;
console.log(a, b, c); // 10 20 30跳过元素
const [first, , third] = arr; // 跳过第二个元素
console.log(first, third); // 10 30默认值
const [x, y = 100] = [5]; // y 未提供,使用默认值
console.log(x, y); // 5 100剩余元素
const [head, ...rest] = [1, 2, 3, 4];
console.log(head); // 1
console.log(rest); // [2, 3, 4]交换变量(无需临时变量)
let a = 1,
b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1== 的机制
在 JavaScript 中有 == 和 === 两种相等运算符。=== 严格相等运算符会先比较两者类型,若类型不同则直接返回 false。 而 == 宽松比较,数据类型相同比较数值,若两者数据类型不用,会先进行隐式转换,再进行值的比较。
转换规则:
NaN的特例:Nan与任何值(包括自身)比较都返回 false,也是唯一不等于自身的值。null与undefined:null与undefined两者互等(null==undefinedtrue),且与其它值(除了自身)都不相等。
数字类型趋势:布尔值、字符串、对象 在进行隐式转换时都有转换为数字类型的趋势。
- 布尔值:true 转换为 1,false 转换为 0。
- 字符串:非数字字符串转换为 Nan,数字字符串转换为对应的数字。空字符转换为 false。
- 对象:对象会调用
valueOf()或toString()转换为原始值(先调用valueOf()获取原始值,若不是原始值则调用toString()),再按上述规则比较。 null和undefined不进行任何转换。
下面是一些转换过程示例:
{a: 1} == 1
// 1. 类型不同,进行转换。{a:1} 先调用 valueOf 返回对象自身:{a: 1},1 为原始值
{a: 1} == 1
// 2. valueOf() 返回非原始值,继续调用 toString() 方法,obj.toString(); // 返回 "[object Object]"
"[object Object]" == 1
// 3. 字符串与数字比较,字符串转数字,Number("[object Object]") 返回 NaN
Nan == 1
// 4. NaN 与任何值都不相等,返回 false
false[1,2] = '1'
// 1. [1,2]通过 valueOf()返回数组自身:[1, 2] (非原始值)
[1,2] == '1'
// 2. 调用 toString(),数组的 toString() 相当于 join(',')返回 "1,2"
"1,2" == '1'
// 3. 两边都是字符串,直接比较字符串内容:
falseES6 新增特性
思考
ES6 新增了哪些特性?
一、变量与作用域
let/const
块级作用域,解决var的变量提升和全局污染问题。let a = 1; const PI = 3.14; // 不可重新赋值
二、函数与参数
- 箭头函数(Arrow Functions)
简洁语法,无自己的this,继承外层作用域。const add = (a, b) => a + b; - 默认参数
function greet(name = 'Guest') { ... } - 剩余参数(Rest Parameters)
function sum(...nums) { return nums.reduce((a, b) => a + b); } - 扩展运算符(Spread Operator)
const arr = [1, ...[2, 3], 4]; // [1, 2, 3, 4]
三、对象与数组
- 解构赋值(Destructuring)
const [a, b] = [1, 2]; const { name, age } = user; - 对象字面量增强
const name = 'Alice'; const obj = { name, getName() { return this.name; } }; - 模板字符串(Template Literals)
const msg = `Hello, ${name}!`;
四、模块化
import/export// math.js export const add = (a, b) => a + b; export default subtract; // main.js import subtract, { add } from './math.js';
五、类与面向对象
class语法糖class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name); } } class Dog extends Animal { ... }
六、异步编程
- Promise(标准化异步控制流)
fetch('/api').then(res => res.json()).catch(err => ...); Symbol
唯一且不可变的原始类型,常用于避免属性名冲突。const id = Symbol('id'); obj[id] = 123;
七、数据结构
Map/Set/WeakMap/WeakSetconst map = new Map([['a', 1], ['b', 2]]); const set = new Set([1, 2, 2]); // {1, 2}
八、其他重要特性
for...of循环:遍历可迭代对象(如数组、Map、Set、字符串等)。for (const item of [1, 2, 3]) console.log(item);- 模块化的
import()动态导入(虽在 ES2020 正式标准化,但源于 ES6 模块思想)。
ES6 的核心价值在于:
- 引入块级作用域和常量;
- 提供类、模块、解构、箭头函数等现代语法;
- 标准化Promise 和 新的集合类型;
- 极大提升了代码的可读性、可维护性与工程化能力。
null 与 undefined
思考
null与undefined的区别?
null 和 undefined 都表示“没有值”,但它们的含义和使用场景不同:
undefined表示一个变量已经声明,但尚未被赋值;或者访问对象中不存在的属性;或者函数调用时未提供某个参数。它是 JavaScript 引擎自动赋予的“默认无值”状态,代表“缺少值”或“未初始化”。null是一个可以被开发者显式赋值的值,表示“有意为空”或“没有对象”。它通常用于表明某个变量或属性当前不指向任何对象,是程序逻辑中主动设置的“空值”。
在类型上,typeof undefined 返回 'undefined',而 typeof null 错误地返回 'object'(这是 JavaScript 的历史遗留 bug)。
在相等性上,null == undefined 为 true(抽象相等比较中的特例),但 null === undefined 为 false(严格相等,类型不同)。
因此,undefined 是“系统认为这里没值”,null 是“开发者说这里没值”。
区分数组与对象
思考
如何区分数组与对象?
typeof 只能获取基本数据类型,对于像数组、对象、null 等都会返回 'object'。下面是几种判断数组与对象的方法:
1. Array.isArray()
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
Array.isArray(arr); // true
Array.isArray(obj); // false2. instanceof
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false3. constructor
const arr = [1, 2, 3];
arr.constructor === Array; // true
// 但容易被伪造
const fakeArr = { constructor: Array };
fakeArr.constructor === Array; // true
// 或被改写
arr.constructor = Object;
arr.constructor === Array; // false4. Object.prototype.toString.call()
const arr = [1, 2, 3];
const obj = { a: 1 };
Object.prototype.toString.call(arr); // "[object Array]"
Object.prototype.toString.call(obj); // "[object Object]"
// 这是 Array.isArray 的底层原理提示
Object.prototype.toString.call() 是判断数据类型最全面的方法,不仅能判断基本类型,众多引用类型都能判断。
// 基本数据类型检测
Object.prototype.toString.call('hello'); // "[object String]"
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(123n); // "[object BigInt]"
// 对象类型(能区分具体类型)
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(/regex/); // "[object RegExp]"
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(new Set()); // "[object Set]"
Object.prototype.toString.call(new WeakMap()); // "[object WeakMap]"
Object.prototype.toString.call(new WeakSet()); // "[object WeakSet]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call(JSON); // "[object JSON]"
// 跨 iframe 也有效
Object.prototype.toString.call(iframeArray); // "[object Array]"