回顾JavaScript(原理篇)

前言

曾经的javascript是跟着B站的视频学习的,也没有好好的看相关的书籍,导致在相关开发中,因不熟悉原理和方法,导致部分功能写的较为臃肿,因此打算重新回顾梳理一下前端之根本------JavaScript

1.JS原理

要回顾JS,首先应该明确JS的原理与相关执行逻辑,正所谓知其然,知其所以然,为了更好的了解JS的原理,首先要理解以下几个概念。

  • JS引擎
  • 执行上下文
  • JS标签
  • 回调函数
  • 调用栈
  • 事件循环
  • 作用域与作用域链
  • 原型与原型链

JS引擎

理解:专门处理JS文件的虚拟机

作用:

  1. 编译代码:将JS代码编译为不同CPU机器对应的汇编语言代码,再进而转化为机器语言,供电脑底层识别
  2. 垃圾回收:按照对应的垃圾回收算法,将一些不再被使用的内存释放掉
  3. 分配内存:JS也存在堆栈,堆存放一些对象等,栈则存放方法以及一些基础的数据类型
  4. 执行代码:执行JS代码,让页面动起来

种类:

  • JScore:WebKit 默认的内嵌 JS 引擎
  • V8:目前最主流JS引擎,性能yyds
  • Hermes:FaceBook推出的一款JS引擎
  • QuickJS:新兴的比较有潜力的JS引擎

最后再提一下最常用的V8引擎

V8引擎优点

  1. 基于即时编译JIT,先进的算法
  2. 优秀的垃圾回收算法及机制(内存管理)
  3. 多线程执行JS代码,以提高效率
  4. 跨平台支持,兼容多操作系统
  5. 开放源代码

V8引擎应用场景

  1. Web浏览器
  2. Node.js
  3. Electron
  4. 游戏开发

执行上下文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)是否有待处理的任务。如果队列中有任务,则将任务从队列中取出,并执行相应的回调函数。执行完一个任务后,再检查队列是否还有其他任务,以此类推,形成循环。

步骤:

  1. 执行同步任务:按照代码的顺序执行同步任务。
  2. 执行微任务(Microtask):处理由 Promise、DOM树变化等产生的微任务。微任务会优先于下一个宏任务执行。
  3. 执行渲染:更新页面的渲染状态。
  4. 执行宏任务(Macrotask):处理定时器回调、事件回调、IO操作、网络请求等宏任务。宏任务的优先级次于微任务。
  5. 重复以上步骤:不断循环,直到所有任务都被处理完毕。

常见案例

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+的新特性,是指使用花括号 {} 创建的作用域(语句块),并且在语句块中声明的语句或变量只在当前语句块中起作用。
    通过 letconst 关键字可以在任意代码块(例如 if 语句、for 循环等)内创建块级作用域。
scss 复制代码
举例
function func() {
    if (true) { 
        let a = 1 ; 
        console.log(a); // 输出:1 
    } 
    console.log(a); // 报错:a is not defined
}
  func();

作用域链

理解:由多个嵌套作用域形成的链条就是作用域链

步骤:

  1. 当查找某个变量a的时候,会先从当前作用域中查找
  2. 如果未找到,就会从其父级作用域(外部一层)的变量对象中查找
  3. 如果还是未找到,就继续从其父级作用域(外部一层)的变量对象中查找
  4. 一直找到全局作用域的变量对象,全局作用域就相当于一个兜底的存在
  5. 如果还是未找到,就报错 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

图解

构造函数--->实例
  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象(这样this就指向了新对象)
  3. 执行构造函数中的代码(为新对象添加实例属性和实例方法)
  4. 返回新对象
    引用自一文搞懂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

图解

相关推荐
路近岸7 分钟前
Angular-生命周期及钩子函数
前端·javascript·angular.js
liuweidong08022 小时前
【Pandas】pandas Series rtruediv
前端·javascript·pandas
我想学LINUX4 小时前
【2024年华为OD机试】(C卷,100分)- 攀登者1 (Java & JS & Python&C/C++)
java·c语言·javascript·c++·python·游戏·华为od
然后就去远行吧6 小时前
小程序组件 —— 31 事件系统 - 事件绑定和事件对象
前端·javascript·小程序
夕阳_醉了6 小时前
如何在JS里进行深拷贝
开发语言·javascript·ecmascript
疯狂的沙粒7 小时前
对React的高阶组件的理解?应用场景?
前端·javascript·react.js·前端框架
星云code8 小时前
【HM-React】08. Layout模块
javascript·react.js·ecmascript
互联网-小阿宇9 小时前
【HTML+CSS+JS+VUE】web前端教程-31-css3新特性
前端·javascript·css
NoneCoder9 小时前
JavaScript系列(24)--内存管理机制详解
开发语言·javascript·ecmascript
han_9 小时前
为实现前端截图功能,我的dom-to-image踩坑之旅!
前端·javascript