前言
曾经的javascript是跟着B站的视频学习的,也没有好好的看相关的书籍,导致在相关开发中,因不熟悉原理和方法,导致部分功能写的较为臃肿,因此打算重新回顾梳理一下前端之根本------JavaScript
1.JS原理
要回顾JS,首先应该明确JS的原理与相关执行逻辑,正所谓知其然,知其所以然,为了更好的了解JS的原理,首先要理解以下几个概念。
- JS引擎
- 执行上下文
- JS标签
- 回调函数
- 调用栈
- 事件循环
- 作用域与作用域链
- 原型与原型链
JS引擎
理解:专门处理JS文件的虚拟机
作用:
- 编译代码:将JS代码编译为不同CPU机器对应的汇编语言代码,再进而转化为机器语言,供电脑底层识别
- 垃圾回收:按照对应的垃圾回收算法,将一些不再被使用的内存释放掉
- 分配内存:JS也存在堆栈,堆存放一些对象等,栈则存放方法以及一些基础的数据类型
- 执行代码:执行JS代码,让页面动起来
种类:
- JScore:WebKit 默认的内嵌 JS 引擎
- V8:目前最主流JS引擎,性能yyds
- Hermes:FaceBook推出的一款JS引擎
- QuickJS:新兴的比较有潜力的JS引擎
最后再提一下最常用的V8引擎
V8引擎优点
- 基于即时编译JIT,先进的算法
- 优秀的垃圾回收算法及机制(内存管理)
- 多线程执行JS代码,以提高效率
- 跨平台支持,兼容多操作系统
- 开放源代码
V8引擎应用场景
- Web浏览器
- Node.js
- Electron
- 游戏开发
执行上下文RunTime
JS可以调用浏览器相关的API,比如Window对象,DOM等以及JS的事件循环(Event Loop)和事件队列(Callback Queue)等,都归功于RunTime
JS标签
xml
<script>常规</script>
<script async>异步</script>
<script defer>延迟</script>
Script
当HTML遇到JS时,会等待JS(请求+执行),结束后才执行html
Async Script
Html和Js请求都是异步执行,JS请求成功后,html等待js去执行
Defer Script
Html和Js请求都是异步执行,JS请求成功后,也要等待HTML的解析
回调函数
理解:函数作为一个参数传递到另一个函数中,就是一个钩子
作用:解决异步编程问题
弊端:可能产生回调地狱的问题
什么是异步?
ini
let res = $ajax.get('...');
console.log(res);
此时的res是undefined,因为ajax请求是异步的,打印res的时候,ajax还没执行完
javascript
$ajax.get('...', (res)=> {
console.log(res);
})
此时的res是正确的返回结果
常见的异步执行:
- 定时器
- 建立网络连接
- 读取网络流数据
- 向文件写入数据
- Ajax提交
- 请求数据库服务
JS引擎其实并不提供异步的支持,异步支持主要依赖于运行环境(浏览器或Node.js)
调用栈
调用栈负责管理函数调用的顺序和上下文,并确保函数能够按照正确的顺序执行和返回
起初JS主要用来实现页面相关的交互操作,并且多线程会造成一些复杂的同步问题,因此JS是被设计为单线程运行的
后来HTML5提供了Web Worker,在后台有一个独立的JS,专门用来处理一些耗时的数据操作(不会修改相关DOM等,不影响页面性能)
总结:如果有阻塞产生,便会导致浏览器卡死
go
function func() {
func();
}
func();
如上面的例子,当一个递归没有终止条件时,浏览器便会报错说调用栈溢出
事件循环
理解:负责监听 堆栈 和 回调函数队列
作用:处理异步操作的回调函数
原理:不断地检查任务队列(task queue)是否有待处理的任务。如果队列中有任务,则将任务从队列中取出,并执行相应的回调函数。执行完一个任务后,再检查队列是否还有其他任务,以此类推,形成循环。
步骤:
- 执行
同步任务
:按照代码的顺序执行同步任务。 - 执行
微任务
(Microtask):处理由 Promise、DOM树变化等产生的微任务。微任务会优先于下一个宏任务执行。 - 执行
渲染
:更新页面的渲染状态。 - 执行
宏任务
(Macrotask):处理定时器回调、事件回调、IO操作、网络请求等宏任务。宏任务的优先级次于微任务。 - 重复以上步骤:不断循环,直到所有任务都被处理完毕。
常见案例
css
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
输出结果为3,3,3,而不是1,2,3
因为定时器的回调属于是宏任务,会等待for循环执行结束后再放入执行栈中执行,因此当for循环执行完毕时,i的值已经是3了,因此只会打印3,3,3
解决方案一:闭包
原理: 将 i 作为参数传递给闭包函数,使得输出从 0 开始,在每次迭代中,闭包函数就会捕获到当前迭代的值,并在 setTimeout
中使用。
javascript
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000 * j);
})(i);
}
解决方案二:let声明变量
let
声明的变量具有块级作用域,每次迭代循环时会创建一个新的变量绑定,这样就保证每个迭代的 setTimeout
获取的 i
都是其对应的值。
css
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
作用域与作用域链
作用域
- 全局作用域
顾名思义就是全局性质的作用域,在任何地方都可以访问的到的最外层作用域,全局作用域位于所有函数之外
go
举例
var a = 1;
function func(){
console.log(a);
}
func(); //1
- 函数作用域
顾名思义就是只能在函数中使用,或指在函数内部声明的变量,函数作用域可以防止变量和函数名称冲突,并保护内部变量不被外部访问。
ini
举例
var a = 1;
function func(){
var a=2
var b=1;
console.log(a);
}
func(); //2
console.log(b); //报错:访问不到函数内的b,外部又没有,因此是undefined
- 块级作用域
ES6+的新特性,是指使用花括号{}
创建的作用域(语句块),并且在语句块中声明的语句或变量只在当前语句块中起作用。
通过let
和const
关键字可以在任意代码块(例如 if 语句、for 循环等)内创建块级作用域。
scss
举例
function func() {
if (true) {
let a = 1 ;
console.log(a); // 输出:1
}
console.log(a); // 报错:a is not defined
}
func();
作用域链
理解:由多个嵌套作用域形成的链条就是作用域链
步骤:
- 当查找某个变量a的时候,会先从当前作用域中查找
- 如果未找到,就会从其父级作用域(外部一层)的变量对象中查找
- 如果还是未找到,就继续从其父级作用域(外部一层)的变量对象中查找
- 一直找到全局作用域的变量对象,全局作用域就相当于一个兜底的存在
- 如果还是未找到,就报错 a is not defined
代码举例
scss
var a = 1;
function func1() {
var b = 2;
function func2() {
var c = 3;
console.log(a); //1
console.log(b); //2
console.log(c); //3
}
console.log(a); //1
console.log(b); //2
console.log(c); //c is not defined
}
console.log(a); //1
console.log(b); //b is not defined
console.log(c); //c is not defined
func1();
原型与原型链 (继承的机制)
每个对象都有一个原型(prototype)
,并且通过原型链(prototype chain)
连接在一起。
要说原型与原型链,首先应该说说构造函数Constructor
ini
定义构造函数
function Person(name, age) {
this.name = name; this.age = age; }
创建实例
const p = new Person('张三', 18);
JS通过构造函数
来生成实例
,但是这些实例都是一些个性化的内容,正常情况下它们应该拥有一些公共的属性,比如说都会说话,都会走路等等...
但是我们无法在构造函数中共享一些公共的属性,于是原型对象诞生了,它的作用就是用来存储这个构造函数的公共属性和方法
正如ChatGPT所说,如果我们想在构造函数中定义公共属性,可以使用构造函数的 prototype 属性
原型对象
- JS的每个函数在创建的时候,都会生成一个属性
prototype
- 这个prototype属性指向一个对象,这个对象就是此函数的
原型对象
- 这个
原型对象
中又有个属性为constructor
,指向该函数
图解
原型链
ini
构造函数
function Preson(name, age) {
this.name = name;
this.age = age; }
创建公共方法
Preson.prototype.say = function () {
console.log('哈哈哈哈'); }
创建一个实例对象
const p= new Preson('张三', 18);
p.say(); // 哈哈哈哈
根据上述例子,say
方法是定义在p
的构造函数的原型上的,但是p
却可以调用,这就是因为有原型链
的存在,使得p
可以一直沿着原型链去找say
这个方法,最终在Person
的原型上找到了这个方法。
- 当调用某个方法时
- JS会先显式的在该对象身上找
- 当找不到时,便会去找那些隐式的同名方法
- 找的规则就是沿着原型链一层一层的去找
- 一直找到终极对象Object,再往上找时,就是null,因此原型链的尽头就是null
-
- 如果找到了,就调用/使用
-
- 如果还是找不到,就报错 func is not defined
图解
构造函数--->实例
- 创建一个新对象
- 将构造函数的作用域赋值给新对象(这样this就指向了新对象)
- 执行构造函数中的代码(为新对象添加实例属性和实例方法)
- 返回新对象
引用自一文搞懂JS原型与原型链(超详细,建议收藏) - 掘金 (juejin.cn)
显式原型与隐式原型
p.__proto__ === Person.prototype
ini
function Person(name, age) {
this.name = name; this.age = age;
}
创建实例
const p = new Person('张三', 18);
一句话概括就是 实例 的隐式原型__proto__
指向 构造函数 的显式原型prototype
图解