JavaScript是Web开发的核心语言,但是很多人对JavaScript的理解只停留在表面,会写一些简单的DOM操作和AJAX,却对JavaScript的核心概念理解不深。闭包、原型链、作用域,这三个概念是JavaScript的核心,也是面试中最常考的知识点,同时也是很多人感到困惑的地方。
今天就来深入讲解JavaScript的这三个核心概念,帮助大家真正理解JavaScript的工作原理,写出更优雅、更高效的代码。
一、作用域(Scope)
作用域是指变量和函数的可访问范围,也就是在代码的哪些地方可以访问到某个变量或函数。JavaScript的作用域分为全局作用域、函数作用域和块级作用域(ES6新增)。
1. 全局作用域:
在代码最外层定义的变量和函数,拥有全局作用域,在代码的任何地方都可以访问。
var globalVar = "我是全局变量";
function globalFunc() {
console.log("我是全局函数");
}
console.log(globalVar); // 可以访问
globalFunc(); // 可以调用在浏览器中,全局作用域就是window对象,全局变量和函数都是window对象的属性和方法。
2. 函数作用域:
在函数内部定义的变量和函数,拥有函数作用域,只能在函数内部访问,外部无法访问。
function test() {
var localVar = "我是局部变量";
console.log(localVar); // 可以访问
}
test();
console.log(localVar); // 报错,localVar is not defined函数作用域的特点:
- 函数内部可以访问外部的变量(包括全局变量)
- 函数外部不能访问函数内部的变量
- 不同函数之间的变量相互独立,互不影响
3. 块级作用域(ES6):
ES6之前,JavaScript只有全局作用域和函数作用域,没有块级作用域,这导致了很多问题。ES6引入了let和const关键字,支持块级作用域。
块级作用域是指在{}(代码块)内部定义的变量,只能在代码块内部访问。
if (true) {
var a = "var定义的变量";
let b = "let定义的变量";
const c = "const定义的变量";
}
console.log(a); // 可以访问,var没有块级作用域
console.log(b); // 报错,b is not defined
console.log(c); // 报错,c is not definedlet和const的区别:
- let:声明的变量可以重新赋值
- const:声明的常量不能重新赋值(但是对象的属性可以修改)
4. 作用域链:
当访问一个变量时,JavaScript引擎会先在当前作用域查找,如果找不到,就到上一级作用域查找,直到全局作用域。这种逐级查找的机制,就是作用域链。
var a = 1;
function outer() {
var b = 2;
function inner() {
var c = 3;
console.log(a); // 1,沿着作用域链找到全局变量a
console.log(b); // 2,沿着作用域链找到outer函数的变量b
console.log(c); // 3,当前作用域的变量c
}
inner();
}
outer();作用域链的方向是从内到外,内部可以访问外部,外部不能访问内部。这也是闭包的基础。
二、闭包(Closure)
闭包是JavaScript中最重要也是最容易被误解的概念之一。简单来说,闭包是指有权访问另一个函数作用域中的变量的函数。
1. 什么是闭包:
当一个内部函数引用了外部函数的变量,并且外部函数执行完毕后,内部函数依然存在(被返回或被引用),那么内部函数和它引用的变量就形成了闭包。
function outer() {
var count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
var counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3在这个例子中,outer函数执行完毕后,它的变量count本应该被销毁,但是因为inner函数引用了count,并且inner被返回给了counter,所以count不会被销毁,而是一直保存在内存中。这就是闭包。
2. 闭包的用途:
- 私有变量:利用闭包可以实现私有变量,外部无法直接访问,只能通过特定的方法访问和修改。
function createCounter() {
var count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
var counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined,无法直接访问私有变量- 函数柯里化:利用闭包可以实现函数柯里化,将多参数函数转化为单参数函数。
function add(a) {
return function(b) {
return a + b;
};
}
var add5 = add(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15- 回调函数和事件处理:闭包在回调函数和事件处理中非常常见,可以保存状态。
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2, 3, 4
}, 1000);
})(i);
}如果不用闭包,直接写setTimeout,会输出5个5,因为i是全局变量,循环结束后i=5。
3. 闭包的注意事项:
- 内存泄漏:闭包会使得函数中的变量保存在内存中,不会被垃圾回收,如果使用不当,会导致内存泄漏。不需要的时候,应该手动释放引用。
counter = null; // 释放引用,让垃圾回收回收内存- 循环中的闭包:在循环中使用闭包要注意,上面的例子已经说明了,需要用立即执行函数(IIFE)或者let来解决。
三、原型链(Prototype Chain)
原型链是JavaScript实现继承的核心机制,也是JavaScript最独特的地方。理解原型链,才能真正理解JavaScript的面向对象。
1. 原型对象:
在JavaScript中,每个函数都有一个prototype属性,指向一个对象,这个对象就是原型对象。原型对象包含了可以被所有实例共享的属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log("你好,我是" + this.name);
};
var p1 = new Person("张三");
var p2 = new Person("李四");
p1.sayHello(); // 你好,我是张三
p2.sayHello(); // 你好,我是李四在这个例子中,sayHello方法定义在Person.prototype上,所有Person的实例都可以共享这个方法,不需要每个实例都创建一份,节省了内存。
2. proto和constructor:
每个对象(除了null)都有一个proto属性,指向创建它的构造函数的prototype原型对象。
console.log(p1.__proto__ === Person.prototype); // true每个原型对象都有一个constructor属性,指向对应的构造函数。
console.log(Person.prototype.constructor === Person); // true3. 原型链:
当访问一个对象的属性或方法时,JavaScript引擎会先在对象本身查找,如果找不到,就到它的proto(原型对象)查找,如果还找不到,就到原型对象的proto查找,直到找到或者到达null(Object.prototype.proto === null)。这种逐级查找的链,就是原型链。
console.log(p1.toString()); // [object Object]
// p1本身没有toString方法
// p1.__proto__(Person.prototype)也没有toString方法
// p1.__proto__.__proto__(Object.prototype)有toString方法
// 所以调用的是Object.prototype.toString原型链的结构: p1 → Person.prototype → Object.prototype → null
4. 原型链实现继承:
利用原型链,可以实现JavaScript的继承。
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(this.name + "在吃东西");
};
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数,继承属性
this.breed = breed;
}
// 原型链继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(this.name + "在汪汪叫");
};
var dog = new Dog("旺财", "金毛");
dog.eat(); // 旺财在吃东西(继承自Animal)
dog.bark(); // 旺财在汪汪叫(自己的方法)这是ES6之前常用的继承方式,ES6引入了class语法糖,让继承更简单:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(this.name + "在吃东西");
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(this.name + "在汪汪叫");
}
}但是要注意,class只是语法糖,底层依然是原型链。
四、三者的关系
作用域、闭包、原型链,这三个概念是相互关联的:
- 作用域是基础:作用域决定了变量的可访问范围,作用域链是变量查找的机制。
- 闭包是作用域的应用:闭包利用了作用域链的特性,让函数在外部依然能够访问内部变量。
- 原型链是属性查找的机制:和作用域链类似,原型链是对象属性查找的机制,从对象本身沿着原型链向上查找。
理解了这三个概念,就理解了JavaScript的核心工作原理,能够写出更优雅、更高效的代码,也能在面试中从容应对。
总结
JavaScript的作用域、闭包、原型链是三个核心概念,也是很多人感到困惑的地方。本文从基础到进阶,详细讲解了这三个概念:
- 作用域:全局作用域、函数作用域、块级作用域,作用域链是变量查找的机制。
- 闭包:有权访问另一个函数作用域中变量的函数,用于实现私有变量、函数柯里化、回调函数等,注意内存泄漏问题。
- 原型链:JavaScript实现继承的核心机制,每个对象都有proto指向原型对象,属性查找沿着原型链向上,直到null。
掌握这三个概念,是成为一名优秀JavaScript开发者的必经之路。希望本文能够帮助大家深入理解JavaScript,写出更好的代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录