js 作为前端的中坚力量。那么 javascript 三座大山,你知道是哪些呢?
1️⃣ 作用域和闭包
作用域 指代码当前上下文,控制着变量和函数的可见性和生命周期。最大的作用是隔离变量,不同作用域下同名变量不会冲突。
作用域链 指如果在当前作用域中没有查到值,就会向上级作用域查询,直到全局作用域,这样一个查找过程所形成的链条就被称之为作用域链。
作用域可以堆叠成层次结构,子作用域可以访问父作用域,反之则不行。
作用域具体可细分为四种:全局作用域、模块作用域、函数作用域、块级作用域
全局作用域: 代码在程序的任何地方都能被访问,例如 window 对象。但全局变量会污染全局命名空间,容易引起命名冲突。
模块作用域: 早期 js 语法中没有模块的定义,因为最初的脚本小而简单。后来随着脚本越来越复杂,就出现了模块化方案(AMD、CommonJS、UMD、ES6模块等)。通常一个模块就是一个文件或者一段脚本,而这个模块拥有自己独立的作用域。
函数作用域: 顾名思义由函数创建的作用域。闭包就是在该作用域下产生,后面我们会单独介绍。
块级作用域: 由于 js 变量提升存在变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。典型的案例就是 let 的 for 循环和 var 的 for 循环。
javascript复制代码// var demo
for(var i=0; i<10; i++) {
console.log(i);
}
console.log(i); // 10
// let demo
for(let i=0; i<10; i++) {
console.log(i);
}
console.log(i); //ReferenceError:i is not defined
了解完作用域再来谈谈 闭包: 函数A里包含了函数B,而函数B使用了函数A的变量,那么函数B被称为闭包或者闭包就是能够读取函数A内部变量的函数。
可以看出闭包是函数作用域下的产物,闭包会随着外层函数的执行而被同时创建,它是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包是内层函数对外层函数变量的不释放。
闭包(Closure)是一种在JavaScript中常见的概念,它涉及到函数的嵌套和作用域。闭包允许函数在其被定义的词法作用域之外的地方访问其所创建的作用域中的变量。
在JavaScript中,每当函数被创建时,它都会生成一个闭包。闭包是函数以及其创建时所能访问的所有变量的组合。这些变量可以是函数内部声明的局部变量,也可以是外部作用域中的变量。
理解闭包的概念通常需要理解以下几个关键点:
- 函数嵌套:一个函数内部定义了另一个函数。
- 作用域链:JavaScript中的每个函数都有一个作用域链。作用域链是一个指向变量对象的指针列表,它保证了在函数执行过程中能够访问到所需的变量。
- 闭包的形成:当一个内部函数引用了外部函数的变量,即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。这种现象就是闭包的形成。
让我们通过一个例子来说明闭包的概念:
codefunction outerFunction() {
var outerVariable = 'I am from the outer function';
function innerFunction() {
console.log(outerVariable); // innerFunction可以访问外部函数outerFunction的变量
}
return innerFunction;
}
var closureFunction = outerFunction();
closureFunction(); // 输出: "I am from the outer function"
在这个例子中,我们有一个外部函数outerFunction和一个内部函数innerFunction。innerFunction在定义时可以访问外部函数outerFunction中的outerVariable变量。当outerFunction被调用时,它返回了innerFunction,并将其赋值给变量closureFunction。当我们调用closureFunction时,它打印了外部函数outerFunction的变量outerVariable的值。
请注意,innerFunction能够访问outerVariable是因为它形成了一个闭包,它可以继续访问其创建时的作用域,即outerFunction的作用域。
闭包在JavaScript中有很多实际应用,例如创建私有变量、模块模式、以及事件处理程序等。但要注意,滥用闭包可能导致内存泄漏问题,因为闭包会一直引用它所包含的外部变量,导致这些变量无法被垃圾回收。因此,在使用闭包时,要注意避免不必要的内存占用。
闭包的特征:
- 函数中存在函数;
- 内部函数可以访问外层函数的作用域;
- 参数和变量不会被 GC,始终驻留在内存中;
- 有内存地方才有闭包。
所以使用闭包会消耗内存、不正当使用会造成内存溢出的问题,在退出函数之前,需要将不使用的局部变量全部删除。如果不是某些特定需求,在函数中创建函数是不明智的,闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
以下整理了闭包的应用场景:
// demo1 输出 3 3 3
for(var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// demo2 输出 0 1 2
for(let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// demo3 输出 0 1 2
for(let i = 0; i < 3; i++) {
(function(i){
setTimeout(function() {
console.log(i);
}, 1000);
})(i)
}
/* 模拟私有方法 */
// 模拟对象的get与set方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
/* setTimeout中使用 */
// setTimeout(fn, number): fn 是不能带参数的。使用闭包绑定一个上下文可以在闭包中获取这个上下文的数据。
function func(param){ return function(){ alert(param) }}
const f1 = func(1);setTimeout(f1,1000);
/* 生产者/消费者模型 */
// 不使用闭包
// 生产者
function producer(){
const data = new(...)
return data
}
// 消费者
function consumer(data){
// do consume...
}
const data = producer()
// 使用闭包
function process(){
var data = new (...)
return function consumer(){
// do consume data ...
}
}
const processer = process()
processer()
/* 实现继承 */
// 以下两种方式都可以实现继承,但是闭包方式每次构造器都会被调用且重新赋值一次所以,所以实现继承原型优于闭包
// 闭包
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
// 原型
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
对于闭包的概念好像懂了但又好像缺少了啥?意犹未尽。我也曾也闭包中迷失,但是看完闭包的生命周期让我重新找回自己。
学完就来一波牛刀小试
function test(a, b){
console.log(b);
return {
test: function(c) {
return test(c,a);
}
}
}
var a = test(100);a.test(101);a.test(102);
var b = test(200).test(201).test(202);
var c = test(300).test(301);c.test(302);
// undefined 100 100
// undefined 200 201
// undefined 300 301
2️⃣ 原型和原型链
有对象的地方就有 原型,每个对象都会在其内部初始化一个属性,就是prototype(原型),原型中存储共享的属性和方法。当我们访问一个对象的属性时,js引擎会先看当前对象中是否有这个属性,如果没有的就会查找他的prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象。这么一个寻找的过程就形成了 原型链 的概念。
理解原型最关键的是理清楚__proto__、prototype、constructor三者的关系,我们先看看几个概念:
- __proto__属性在所有对象中都存在,指向其构造函数的prototype对象;prototype对象只存在(构造)函数中,用于存储共享属性和方法;constructor属性只存在于(构造)函数的prototype中,指向(构造)函数本身。
- 一个对象或者构造函数中的隐式原型__proto__的属性值指向其构造函数的显式原型 prototype 属性值,关系表示为:instance.__proto__ === instance.constructor.prototype
- 除了 Object,所有对象或构造函数的 prototype 均继承自 Object.prototype,原型链的顶层指向 null:Object.prototype.__proto__ === null
- Object.prototype 中也有 constructor:Object.prototype.constructor === Object
- 构造函数创建的对象(Object、Function、Array、普通对象等)都是 Function 的实例,它们的 __proto__ 均指向 Function.prototype。
看起来是不是有点乱??别慌!!一张图帮你整理它们之间的关系
相同的配方再来一刀
const arr = [1, 2, 3];
arr.__proto__ === Array.prototype; // true
arr.__proto__.__proto__ === Object.prototype; // true
Array.__proto__ === Function.prototype; // true
在JavaScript中,原型(Prototype)和原型链(Prototype Chain)是与对象和继承相关的重要概念。
- 原型(Prototype): 在JavaScript中,每个对象都有一个原型对象。原型对象是一个普通的对象,它包含共享属性和方法,可以被其他对象继承。在创建对象时,可以使用构造函数或者直接使用对象字面量的方式,而每个构造函数都有一个原型对象与之关联。如果使用构造函数创建对象,那么新创建的对象将继承构造函数的原型对象上的属性和方法。
例如,我们可以通过构造函数Person来创建一个对象,并让该对象继承Person的原型对象:
javascriptCopy codefunction Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
var person1 = new Person('Alice');
person1.sayHello(); // 输出: "Hello, my name is Alice"
在上面的例子中,我们使用Person构造函数创建了person1对象。person1继承了Person.prototype上的sayHello方法。
- 原型链(Prototype Chain): 当我们试图访问对象的属性或方法时,如果该对象自身没有这个属性或方法,JavaScript会在原型链中继续查找,直到找到该属性或方法或者抵达原型链的末端。原型链的形成是通过每个对象的[[Prototype]]属性来实现的,这个属性指向对象的原型。
当我们尝试访问person1的属性或方法时,首先会查找person1自身是否有该属性或方法,如果没有,它会继续在Person.prototype中查找。如果Person.prototype也没有,那么它会继续在Person.prototype的原型对象上查找,即Object.prototype。Object.prototype是位于原型链的顶端,它是所有对象的最终原型。
示例:
codeconsole.log(person1.hasOwnProperty('name')); // 输出: true,person1对象自身拥有'name'属性
console.log(person1.hasOwnProperty('sayHello')); // 输出: false,'sayHello'方法在person1对象的原型链上
在上面的例子中,person1对象自身拥有name属性,所以hasOwnProperty方法返回true。而sayHello方法是在Person.prototype上定义的,所以person1对象通过原型链继承了sayHello方法。
这样的原型链继承机制使得JavaScript对象可以共享属性和方法,提供了更高效的对象创建和继承方式。但在某些情况下,原型链继承也可能导致一些意外问题,因此在设计对象继承时需要小心处理。
3️⃣ 异步和单线程
JavaScript 是一种单线程(Single-threaded)脚本语言,这意味着它在同一时间只能执行一个任务或代码块。这与其他一些编程语言不同,比如 Java 和 C++,它们可以通过多线程实现并行执行多个任务。
单线程的 JavaScript 是由其最初设计的用途决定的:它是为了在浏览器中操作 DOM (文档对象模型) 和响应用户交互而创建的。在这样的环境中,多线程会导致许多复杂性和竞态条件的问题。因此,JavaScript 选择了单线程的执行模型。
异步编程是 JavaScript 中的另一个关键概念,它允许在等待某些任务完成时,继续执行后续代码,而不会阻塞整个程序的执行。异步编程对于处理网络请求、文件读写、定时器等场景非常重要,因为这些任务可能需要一定的时间才能完成。
在 JavaScript 中,常见的异步编程模式包括:
- 回调函数(Callback):将一个函数作为参数传递给另一个函数,在异步任务完成时调用回调函数来处理结果。
- Promise:Promise 是一种更加现代的异步编程方式,它可以更清晰地表示异步操作的成功或失败,并可以通过链式调用来处理多个异步任务。
- Async/Await:Async/Await 是 ECMAScript 2017 引入的异步编程语法糖,它基于 Promise,并以更像同步代码的方式编写异步代码。
通过异步编程,JavaScript 可以在执行耗时的操作时继续响应其他任务,保持了单线程的特性,并提供了高效的并发处理能力。在浏览器环境中,异步编程也能避免阻塞用户界面的问题,使用户能够继续与页面交互,同时后台执行一些复杂任务。
需要注意的是,虽然 JavaScript 是单线程的,但现代浏览器和 Node.js 等运行时环境提供了 Web Workers 和 Worker Threads 等机制,允许开发者在单独的线程中执行某些任务,从而实现类似多线程的效果。但这些额外的线程通常用于执行计算密集型任务,而不是操作 DOM 或进行其他可能导致竞态条件的操作。
JavaScript 是 单线程 语言,意味着只有单独的一个调用栈,同一时间只能处理一个任务或一段代码。队列、堆、栈、事件循环构成了 js 的并发模型,事件循环 是 JavaScript 的执行机制。
为什么js是一门单线程语言呢?最初设计JS是用来在浏览器验证表单以及操控DOM元素,为了避免同一时间对同一个DOM元素进行操作从而导致不可预知的问题,JavaScript从一诞生就是单线程。
既然是单线程也就意味着不存在异步,只能自上而下执行,如果代码阻塞只能一直等下去,这样导致很差的用户体验,所以事件循环的出现让 js 拥有异步的能力。
以上就是关于js三座大山的全部内容。什么?还不懂,告诉我ip地址我顺着网线过去教你!!
总结
学好 JavaScript 需要掌握以下要点:
- 基本语法:了解 JavaScript 的基本语法,包括变量声明、数据类型、运算符、条件语句、循环语句等。
- 数据类型和类型转换:熟悉 JavaScript 的数据类型,包括原始数据类型(字符串、数字、布尔值、null、undefined)和复杂数据类型(对象、数组、函数等),以及类型转换的规则。
- 函数:深入理解函数的定义、调用、参数传递和返回值,并掌握闭包、匿名函数和箭头函数等概念。
- 对象和面向对象编程:了解 JavaScript 中的对象、原型和原型链,学习面向对象编程的基本思想和实现方式。
- 异步编程:熟悉 JavaScript 的异步编程机制,包括回调函数、Promise、Async/Await 等,以及处理异步操作的常见场景。
- DOM 操作:掌握使用 JavaScript 操作 DOM 元素,实现页面动态交互和事件处理。
- 事件处理:了解事件的概念和事件处理程序的注册和触发方式。
- AJAX 和 Fetch:学习如何使用 AJAX 和 Fetch 进行网络请求,与服务器进行数据交互。
- ES6+ 特性:了解 ECMAScript 6 及以上版本带来的新特性,如箭头函数、解构赋值、模板字符串、类、模块等。
- 错误处理:掌握异常处理的技巧,使用 try-catch 块来捕获和处理异常。
- 调试工具:熟悉浏览器提供的开发者工具,包括调试器和控制台,以及其他调试工具如 Lint 工具等。
- 代码优化:学习编写高效、可读性好的 JavaScript 代码,避免常见的性能问题。
- 框架和库:了解常见的 JavaScript 框架和库,如 React、Vue、Angular、jQuery 等,并根据需要学习它们的使用。
- 测试:了解 JavaScript 测试的重要性,学习使用测试框架和工具进行单元测试和集成测试。
- 学以致用:不断实践和构建项目,通过实际项目来巩固所学的知识,并不断拓展自己的技能。
以上是学好 JavaScript 的一些重要要点。JavaScript 是一门非常灵活和强大的语言,掌握好基础并不断深入学习,将能够编写出高质量的前端和后端应用。记得学习过程中保持耐心和持续学习的态度,多写代码和与其他开发者交流,能够更好地提高编程能力。