变量声明和作用域
var
, let
, 和 const
是JavaScript中用于声明变量的关键字,它们在作用域和提升行为方面有所不同。
1.
var
:
var
声明的变量具有函数作用域(如果在函数内声明)或全局作用域(如果在函数外声明)。var
声明的变量会被提升到其所在作用域的顶部,但仅限于变量声明(不包括赋值)。这意味着在声明之前使用变量会返回undefined
。var
可以被重复声明,这可能导致意外的行为,特别是在大型代码库或团队协作中。
2.
let
:
let
声明的变量具有块级作用域,即它们仅在声明它们的代码块(如if
语句、for
循环等)内有效。let
声明的变量不会被提升,尝试在声明之前访问变量会导致一个ReferenceError
。let
提供了块级作用域,这有助于避免变量提升带来的问题,并且使得代码更加清晰和易于管理。
3.
const
:
const
声明的变量也具有块级作用域,并且必须在声明时初始化。const
声明的变量同样不会被提升,且尝试修改已初始化的const
变量会导致错误。- 使用
const
声明常量可以防止变量被重新赋值,有助于代码的可读性和维护性。
变量提升(Hoisting):
- 变量提升是JavaScript中的一种机制,其中变量和函数声明在代码执行之前被移动到其所在作用域的顶部。
var
声明的变量和函数声明(使用function
关键字)都会被提升。let
和const
声明的变量也会被提升,但它们不会被初始化,因此在声明之前访问它们会导致ReferenceError
。
块级作用域与函数作用域:
- 函数作用域:在函数内声明的变量或函数只在该函数内部可见。
- 块级作用域:使用
let
和const
声明的变量仅在包含它们的代码块(如if
语句、for
循环等)内可见。这有助于限制变量的作用范围,减少全局变量的使用,从而降低代码中潜在的冲突和错误。
举例说明
例子 1: var
的变量提升和函数作用域
console.log(a); // 输出:undefined,因为 var a 被提升到了作用域顶部,但未初始化
var a = 5;
function exampleVar() {
if (true) {
var a = 10; // 同样被提升到函数作用域顶部
}
console.log(a); // 输出:10,因为 a 在 if 块内被重新赋值
}
exampleVar();
console.log(a); // 输出:5,函数作用域外的 a 未被影响
例子 2: let
的块级作用域和提升行为
console.log(b); // ReferenceError: b is not defined,因为 let b 没有被提升
let b = 5;
function exampleLet() {
if (true) {
let b = 10; // b 只在 if 块内有效
}
console.log(b); // 输出:5,块级作用域外的 b 未被影响
}
exampleLet();
例子 3: const
的块级作用域和提升行为
console.log(c); // ReferenceError: c is not defined,因为 const c 没有被提升
const c = 5;
function exampleConst() {
if (true) {
const c = 10; // c 只在 if 块内有效
}
console.log(c); // 输出:5,块级作用域外的 c 未被影响
}
exampleConst();
闭包
闭包的定义和用途
定义:
闭包是JavaScript中的一个核心概念,它允许一个函数访问并操作函数外部的变量。更具体地说,当一个内部函数被其外部函数返回并在其他地方被调用时,它仍然可以访问外部函数的变量,即使外部函数已经执行完毕。这些变量不会被垃圾回收机制回收,因为内部函数仍然持有对它们的引用。
用途:
1.数据封装和隐私:闭包可以创建私有变量,因为它们只能通过闭包内部的函数访问。
2.模块化代码:闭包可以帮助组织代码,将相关的功能封装在一起。
3.维持状态:闭包可以用来保存和维持函数的状态,即使函数执行完毕后,状态仍然可以被后续的函数调用所访问。
4.回调函数和事件处理:闭包常用于处理异步操作和事件监听,因为它们可以保持必要的上下文信息。
闭包在实际开发中的应用场景
1.模块模式:
通过闭包实现模块化,可以创建具有私有变量和方法的模块。
const counter = (function() {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // 输出:0
counter.increment();
counter.increment();
console.log(counter.value()); // 输出:2
2.回调函数:
在异步操作中,闭包常用于维持状态。
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 3000);
}
wait('Hello, closure!'); // 3秒后输出:Hello, closure!
3.创建工厂函数:
使用闭包创建工厂函数,可以为每个实例创建独立的状态。
function makeMultiplier(x) {
return function(y) {
return x * y;
};
}
const multiplyByTwo = makeMultiplier(2);
const multiplyByThree = makeMultiplier(3);
console.log(multiplyByTwo(4)); // 输出:8
console.log(multiplyByThree(4)); // 输出:12
原型与继承
原型链的概念
在JavaScript中,每个对象都有一个内部链接指向另一个对象,即它的“原型”。这个原型对象自身也有一个原型,直到达到一个终点,这个终点通常是 Object.prototype
。这个由原型对象组成的链条称为“原型链”。
原型链的主要用途是实现继承。当尝试访问一个对象的属性时,如果在该对象上找不到,JavaScript会继续在该对象的原型上查找,这个过程会一直持续到找到该属性或到达原型链的末端。
proto 与 prototype 的区别
-
prototype:
prototype
是函数的一个属性,当使用函数作为构造器创建对象时,通过new
关键字创建的每个对象都会获得一个指向该函数prototype
属性的内部链接。- 通过
prototype
属性,可以向构造函数创建的所有实例共享方法和属性。
-
proto:
__proto__
是一个访问器属性(一个获取/设置函数),它包含对另一个对象的引用,这个对象被称为“原型”。__proto__
不是语言本身的一部分,而是浏览器实现的非标准特性,用于访问对象的原型。现代JavaScript推荐使用Object.getPrototypeOf(obj)
和Object.setPrototypeOf(obj, proto)
来获取和设置对象的原型。- 每个对象都有一个
__proto__
属性,指向它的原型对象。
ES6 的 class 语法糖与传统的原型继承
-
传统原型继承:
- 在ES6之前,JavaScript使用函数和原型链来实现继承。
- 通过设置构造函数的
prototype
属性,并使用new
关键字来创建对象实例,可以实现继承。
-
ES6 的 class 语法糖:
- ES6 引入了
class
关键字,它提供了一种更简洁和直观的方式来定义对象和实现继承。 class
实际上是基于原型继承的语法糖,它并没有引入新的继承模型,而是提供了一种更清晰和易于理解的语法。
- ES6 引入了
// 传统原型继承方式
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log('Hello, my name is ' + this.name);
};
var person = new Person('Alice');
person.greet(); // 输出:Hello, my name is Alice
// ES6 class 语法糖
class PersonES6 {
constructor(name) {
this.name = name;
}
greet() {
console.log('Hello, my name is ' + this.name);
}
}
const personES6 = new PersonES6('Bob');
personES6.greet(); // 输出:Hello, my name is Bob
在上述例子中,Person
和 PersonES6
都创建了一个具有 greet
方法的对象。使用 class
关键字的版本更加简洁和易于理解,但其背后的工作机制与传统原型继承相同。
深浅拷贝的区别
浅拷贝:
- 浅拷贝创建一个新对象,但只复制原始对象的引用值(如对象、数组)的第一层。
- 如果原始对象中的属性是基本类型(如数字、字符串),则复制的是值本身。
- 如果属性是引用类型(如对象、数组),则复制的是引用地址,新旧对象中的引用类型属性仍然指向同一个实例。
深拷贝:
- 深拷贝创建一个新对象,并递归复制原始对象的所有层级,包括嵌套的对象和数组。
- 新对象和原始对象完全独立,修改新对象不会影响原始对象。
如何实现深拷贝
浅拷贝的例子
let original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'traveling']
};
// 浅拷贝
let shallowCopy = Object.assign({}, original);
shallowCopy.hobbies.push('coding');
console.log(original.hobbies); // 输出:['reading', 'traveling', 'coding']
console.log(shallowCopy.hobbies); // 输出:['reading', 'traveling', 'coding']
在这个例子中,shallowCopy
是 original
的浅拷贝。当我们向 shallowCopy.hobbies
添加 'coding' 时,original.hobbies
也发生了变化,因为 hobbies
数组是通过引用传递的。
深拷贝的例子
let original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'traveling']
};
// 深拷贝
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.hobbies.push('coding');
console.log(original.hobbies); // 输出:['reading', 'traveling']
console.log(deepCopy.hobbies); // 输出:['reading', 'traveling', 'coding']
在这个例子中,我们使用 JSON.parse(JSON.stringify(object))
来创建 original
的深拷贝。现在,deepCopy
是完全独立的,修改 deepCopy.hobbies
不会影响到 original.hobbies
。
简单解释
- 浅拷贝:创建了一个新对象,但对象内部的复杂类型(如数组、对象)仍然是引用原来的值。
- 深拷贝:创建了一个新对象,并且递归复制了所有层级的属性,包括复杂类型的属性,使得新对象与原对象完全独立。