1. JS 的基本概念
JavaScript(简称JS
)是一门脚本语言,用于为网页添加交互效果
和动态功能
。它由三个基本部分组成: ECMAScript
、DOM
和BOM
。
- ECMAScript 是 JavaScript 的核心语言,
规范了 JavaScript 的基本语法、数据类型、流程控制
等。 - DOM(文档对象模型) 是针对 XML 文档的一个 API(应用程序编程接口),也适用于 HTML 文档。它把整个页面映射为一个多层节点结构,通过
DOM API
可以对页面上的任何元素进行操作
。 - BOM(浏览器对象模型) 是指
浏览器提供的一组 JavaScript API
,通过它们可以获取和控制浏览器窗口和标签页等浏览器本身的功能
。BOM 包含了很多对象,比如window、location、navigator
等。
除了这些基本概念,JavaScript 还有一些特性和技术,比如闭包、原型链、异步编程
等,这些都是深入学习 JavaScript
需要了解的内容。
1.1 动态 , 弱类型
JavaScript 的变量是动态的,因为在声明变量时不需要指定变量类型
,变量的类型是在程序运行过程中自动推断
出来的。也就是说,同一个变量在不同的时候
可以存储不同类型
的值。例如,一个变量可以先存储数字类型的值,然后再存储字符串类型的值。
JavaScript 的变量是弱类型的,是因为它们的类型可以随时发生改变
,而且不需要进行类型转换就能进行运算。比如,在 JavaScript 中,一个变量可以存储数字类型的值,另一个变量可以存储字符串类型的值,但是它们仍然可以进行加法操作,并且会自动将字符串转换成数字再进行计算。
这种动态和弱类型的特性使得 JavaScript 在编写灵活性高的应用程序时非常有优势,但也存在一些问题。如果不小心让变量存储了错误类型的值,可能会导致程序出错或者产生意料之外的结果,因此要格外小心处理变量类型的问题。
1.2 变量提升
在使用var时,下面的代码不会报错. 这是因为这个关键字声明的变量会自动提升到函数作用域顶部
代码语言:javascript复制function test(){
console.log(age);
var age = 13;
}
test() // undefined
之所以不会报错,是因为ECMAScript 运行的时候会把它看成等价于如下代码:
代码语言:javascript复制function test1(){
var age;
console.log(age);
age = 13
}
test1()
这就是所谓的变量提升,就是把所有变量声明都拉到函数作用域的顶部.
函数声明也会被提升到作用域顶部,如下所示:
代码语言:javascript复制foo(); // 支持调用,输出 "bar"
function foo() {
console.log("bar");
}
以上代码中,函数 foo 虽然在调用前定义了,但是在代码执行时已经被提升到了作用域顶部,因此可以正常调用并输出 "bar"。
let 和 const 不存在变量提升
2.JS是怎么执行的
2.1 讲解执行大概过程
JavaScript的执行分为两个阶段:编译和执行。
- 编译阶段
当JavaScript代码被加载时,JavaScript引擎会首先把它编译成字节码或机器码。编译阶段包括:
- 词法分析:将代码按照语法规则分解成一个个单独的词汇单元(例如变量名、操作符、关键字等),这些词汇单元称为 Token。
- 语法分析:将词法单元转换为 AST(Abstract Syntax Tree,抽象语法树)。AST是一种用于表示程序代码的树形数据结构,每个节点代表程序中的一个语言单元(如函数、语句、表达式等),可以方便地对代码进行分析和优化。
- 代码生成:将AST转换为可执行代码(机器码或字节码),并将其存储在内存中以供后续执行。
在编译过程中,JavaScript 引擎会执行一些静态检查,如语法检查和类型检查。如果发现代码存在错误,编译阶段会立即停止并抛出错误信息。
- 执行阶段
编译阶段完成后,引擎开始执行代码。代码执行的具体流程是:
- 作用域创建:在进入执行上下文时,JavaScript 引擎会创建一个新的作用域(即执行上下文),并将其加入到执行上下文栈中。
- 变量提升:JavaScript 引擎会扫描当前作用域的所有变量和函数声明,并将它们提升到作用域的顶部,以便正确地处理变量访问和函数调用。这就是前面提到的“变量提升”机制。
- 代码执行:JavaScript 引擎按照编译阶段生成的可执行代码进行执行,逐行解释执行代码,并根据当前状态来更新变量和对象的值。
在执行过程中,JavaScript引擎还会进行一些性能优化,如 JIT(Just-In-Time)编译、内联缓存等,以提高代码的运行速度和效率。
总体来说,JavaScript 的执行流程比较复杂,但是了解其基本原理和流程对于我们编写高效、健壮的 JavaScript 代码非常重要。
2.2 JIT(Just-In-Time)讲解
JIT(Just-In-Time)编译是一种动态编译技术,可以提高JavaScript代码的执行效率。在 JIT 编译中,JavaScript 引擎会将频繁执行的代码动态地编译成机器码并缓存起来,以便后续快速执行。
具体来说,JIT 编译分为三个阶段:
- 解释执行
当 JavaScript 代码被加载时,JavaScript 引擎首先对其进行解释执行,即逐行读取代码并执行相应的操作。由于解释执行需要频繁地解析和执行代码,因此效率较低。
- 编译阶段
在解释执行过程中,JavaScript 引擎会监测代码的执行频率,并对频繁执行的代码进行编译优化。编译过程包括 AST 解析、基础块分析、控制流分析、数据流分析等步骤,最终生成优化后的代码。这些代码被称为机器码。
- 优化阶段
优化阶段是 JIT 编译的核心。在每次执行频繁的代码时,JavaScript 引擎会对其进行优化,并重新生成机器码。这些优化包括内联缓存、函数内联、去除未使用的代码等。优化的目标是尽可能地减少不必要的计算、内存访问等操作,以提高代码的执行效率。
总体来说,JIT 编译技术可以大大提高 JavaScript 代码的执行效率。由于 JIT 编译需要根据代码的实际执行情况进行优化,因此它对于频繁执行的函数和循环特别有效。不过,在少量执行的代码中,JIT 编译可能会增加执行时间,因此需要按需使用。
3. JS的进阶知识点
3.1 闭包
JavaScript 中的闭包是一个非常强大的概念,很多开发者在学习 JavaScript 时都会遇到这个问题。本篇文章将介绍 JavaScript 中的闭包,同时提供一些例子来帮助您更好地理解。
什么是闭包?
首先,我们需要明白闭包是什么。简单的说,闭包是指可以访问独立变量的函数。具体来说,当一个内部函数引用了其外部函数的变量时,就形成了一个闭包。
下面的代码示例将更好地说明这一点:
代码语言:javascript复制// 外部函数
function outerFunction() {
const outerVariable = '123';
// 内部函数
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myInnerFunction = outerFunction();
myInnerFunction(); // 输出:123
在上面的示例代码中,outerFunction
函数返回了 innerFunction
函数。这意味着 innerFunction
可以访问 outerFunction
中定义的变量。因此,当 myInnerFunction()
被调用时,它会输出 I am outside!
。
闭包的优点
闭包的最大优点是它们可以帮助我们隐藏或封装数据。这使得我们可以编写很多高效和安全的代码。其中一个优点是,闭包可以“记住”其父级函数中的数据,即使该函数已经退出并且不再存在。
下面是一个例子,它使用闭包来实现私有变量:
代码语言:javascript复制function createCounter() {
let count = 0;
return function() {
count ;
console.log(count);
}
}
const counterA = createCounter();
counterA(); // 输出:1
counterA(); // 输出:2
const counterB = createCounter();
counterB(); // 输出:1
在这个例子中,createCounter
函数返回一个函数,该函数可以递增计数器并输出值。由于 count
变量只在 createCounter
函数内部定义,因此外部代码无法直接访问它。这使得我们可以安全地保护数据,并确保对其进行处理的代码仅在闭包范围内。
闭包的缺点
虽然闭包非常有用,但它们也有一些缺点。其中,最大的问题是它们可能会浪费内存
。JavaScript 中的垃圾收集器将不会回收闭包中未使用的变量
。如果你创建了很多这样的闭包,那么就可能导致内存泄漏和性能问题
。
下面是一个稍微复杂的例子,它演示了在 JavaScript 中使用闭包的一些缺点:
代码语言:javascript复制function createBigObject() {
const bigObject = new Array(70000).fill('x').join('');
return function() {
console.log(bigObject);
}
}
const myBigObjectFunction = createBigObject();
myBigObjectFunction();
在这个例子中,createBigObject
函数返回了一个包含大量数据的闭包。每次调用 myBigObjectFunction
都会输出这个巨大的字符串。由于 JavaScript 不会回收未使用的闭包变量,因此可能会导致内存泄漏和性能问题。
结论
在本文中,我们介绍了 JavaScript 中的闭包概念,并提供了几个示例来更好地理解它们。闭包是强大而有用的,但也需要小心使用,以避免出现内存泄漏和性能问题。在正确使用闭包的情况下,它们可以使代码更加灵活,可重用,并且能够实现很多高效、安全的功能。
3.2 This
普通函数this 指向Window
this 在 JavaScript 中是一个非常重要的概念。它指的是函数所属的对象,具体取决于函数的调用方式。下面是一些常见的使用方式
1. 作为对象的方法 :
代码语言:javascript复制const person = {
name: 'Alice',
sayHello() {
console.log(`你好,我的名字是 ${this.name}`);
}
};
person.sayHello(); // 输出 "你好,我的名字是 Alice"
在这个例子中,this
指的是 person
对象
2. 作为构造函数:
代码语言:javascript复制function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.name); // 输出 "Alice"
在这个例子中,this
指的是 Person
构造函数创建的新对象。
3. 使用 call 或 apply 显式地设置 this:
代码语言:javascript复制function sayHello() {
console.log(`你好,我的名字是 ${this.name}`);
}
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
sayHello.call(person1); // 输出 "你好,我的名字是 Alice"
sayHello.apply(person2); // 输出 "你好,我的名字是 Bob"
在这个例子中,call 和 apply 被用来将 this 分别设置为 person1 和 person2。
3.3 事件循环
当 JavaScript 运行时,它会将代码分为两类:同步代码和异步代码。同步代码是按顺序执行的,而异步代码则是在后台执行的,不会阻塞主线程。事件循环是 JavaScript 处理异步代码的机制
事件循环是一个不断运行的循环,它会检查消息队列中是否有待处理的消息。如果有,它会将消息从队列中取出并执行。如果没有,它会等待新的消息到达。
下面是一个简单的例子,演示了事件循环的工作原理:
代码语言:javascript复制console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
输入内容:
start ==> end ==> promise ==> timeout
解释:
首先输出 start 和 end,然后遇到 Promise.resolve(),将其放入微任务队列中,继续执行,遇到 setTimeout,将其放入宏任务队列中,最后输出 promise。由于 setTimeout 的延时时间为 0,因此会在当前宏任务执行完毕后立即执行,输出 timeout。
结论:
微任务队列先于 宏任务队列先执行