Js--没写完先看看吧

76 阅读10分钟

知其然不知其所以然,js语言本质上有很多复杂的概念,但是却用一种看起来比较简单的方式体现出来了,开发的时候通常只是简单的使用这些特性,并没有关注内部实现原理。

一:作用域和闭包

1.1 作用域是什么

一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量,这套规则被称为作用域。

作用域作用

  • 收集并维护由所有声明的标识符(变量)组成的 一系列查询
  • 一套规则确定当前执行代码对标识符的访问权限

通俗来说作用域指定一个变量的作用范围。或者或作用域是可访问变量的集合。

1.2 作用域内部原理

包含编译、执行、查询、嵌套、异常。

前提名词解释:

  • 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
  • 作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
编译:包含分词/词法分析、解析/语法分析、代码生成

一段源代码在执行之前会经历三个阶段统称“编译”。

  1. 分词:把字符组成的字符串分解成有意义的代码块,这些代码块称为词法单元。 var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一 个词法单元流数组。
  2. 解析:把词法单元流数组转换成一个由元素逐级嵌套所组成的代表了语法结构的树,称作“抽 象语法树”AST。
  3. 代码生成:将AST转换为可执行代码的过程被称作代码生成(语法树转成机器指令)。
执行:发生编译过程的第三步“生成代码”

执行也分为两步

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!
查询:发生在执行过程的第二步

引擎执行它时,会通过查找变量 a 来判断它是否已声明过。查找的过程由作用域进行协助,但是引擎执行怎样的查找,会影响最终的查找结果。

引擎查询分为两种:LHS查询和RHS查询;

字面意思理解当变量出现在赋值操作左侧就是LHS查询,出现在右侧就是RHS查询。或者根据查询的目的区分如果查询的目的是赋值就是LHS,如果查询的目的是获取变量的值就是RHS。

function foo(a) {
    console.log(a); // 2
}
foo(2)

上述代码包含四处查询:

  1. foo(...)对foo进行了RHS引用(函数调用属于RHS查询,因为函数已经声明好了,只是调用)
  2. 函数传参a=2对a进行了LHS查询:将2赋值给了a
  3. 对console进行RHS引用:console是内置对象,log(...)是其函数,属于调用函数
  4. console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(…)
嵌套:当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套

因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

function foo(a) { 
    console.log( a + b ); 
}
var b = 2;
foo( 2 ); // 4

对 b 进行的 RHS 引用无法在函数 foo 内部完成,但可以在上一级作用域(在这个例子中就 是全局作用域)中完成。

image.png

异常:分为RHS和LHS两种情况

RHS:

  1. 如果RHS查询失败,引擎会抛出ReferenceError(引用错误)异常。
//对b进行RHS查询时,无法找到该变量。也就是说,这是一个“未声明”的变量
function foo(a){
    a = b;  
}
foo();//ReferenceError: b is not defined
  1. 如果RHS查询找到了一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另外一种类型异常:TypeError(类型错误)异常。
function foo(){
    var b = 0;
    b();
}
foo();//TypeError: b is not a function

LHS:

  1. 当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎。
function foo(){
    a = 1;  
}
foo();
console.log(a);//1
  1. 如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常。
function foo(){
    'use strict';
    a = 1;  
}
foo();
console.log(a);//ReferenceError: a is not defined

1.3 函数作用域和块作用域

1.4 闭包

内存泄漏:

当已经不需要使用某块内存时,这块内存还存在着,没有被释放,导致该内存无法被使用。

垃圾回收机制:

主要就是防止内存泄漏,垃圾回收机制会间歇的不定期的寻找不再使用的变量,并释放它们所指向的内存。

var a =1,我们定义了变量a,并且给它赋值为1, 随后我们去改变a的值,a = 100,现在的话我们知道a的值变成了100,那么数值1我们肯定就不会用到了,那么js的垃圾回收机制会帮助我们把数值1给回收了,为了避免内存泄漏

为什么会出现闭包||为什么要使用闭包:

根据Js的语言特性,函数内部可以读取全局变量,但是函数外部是无法读取函数内部变量的,但是有时候我们需要使用或者得到函数内部的变量,这个时候闭包就出现了。闭包的作用和就是可以读取函数内部的变量。

什么是闭包:

闭包的定义很多,最为常见且容易理解的说法是,闭包是可以访问其它函数作用域中变量的函数。

MDN的说法:闭包(closure)是一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。

词法作用域:就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)

闭包作用:
  • 可以在函数内部创建私有变量(包括函数的参数、局部变量、函数内定义的其他函数),并将其隐藏在函数作用域内部,从而防止变量被外部访问和修改。
  • 提供对局部变量的间接访问,可以在多个函数之间共享数据并且避免使用全局变量。
  • 维持变量,使其不会被垃圾回收机制回收。
闭包的工作原理:

当一个函数被调用时,它创建一个执行环境以及相关的作用域链。同时,它创建一个对象,该对象包含了函数内部定义的所有局部变量。当函数执行完毕后,这个对象不会被销毁,而是继续存在于内存之中,直至闭包失效。

闭包的优缺点:
  • 优点:变量不会被回收,可以保存在内存中
  • 缺点:缺点也是因为变量一直存储在内存中,会导致内存泄漏,解决的办法是在退出函数之前将局部变量全部删除或者设置为null
闭包举例:
function outerFunc(){ 
  let count = 0; 
  function innerFunc(){ 
    count++; 
    console.log(count); 
  } 
  return innerFunc; 
} 
 
let closure = outerFunc(); 
closure(); //1 
closure(); //2 

以上代码定义了一个外部函数outerFunc和内部函数innerFunc,并定义了一个内部变量count。在外部函数中,内部函数被定义并返回,这是JavaScript闭包的一个典型例子。

当执行let closure = outerFunc();时,外部函数会被调用,并返回内部函数的引用。变量closure现在包含内部函数的引用,也就是一个闭包。

当执行closure()时,内部函数可以访问并更新count变量。由于该闭包还保持着对函数定义作用域的引用,因此count变量的值在多次调用closure函数时保留,最终的输出分别是1和2,而不是每次都是1。

这就是JavaScript闭包的本质,通过将函数和其相关的作用域绑定到一起,我们可以在函数调用后继续访问相关变量的值。

闭包应用:防抖和节流

防抖:在一定时间n内,多次触发同一事件,只执行最后一次操作。实际应用例如搜索框输入关键字,不断触发outinput事件,防止按钮重复提交

// 输入框输入防抖
search.oninput = (function() {
  let timer = null
  return ()=>{
    if(timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(()=>{
      console.log('发送了ajax请求');
    },500)
  }
})()

节流:在一定时间n内,多次触发同一事件,只执行第一次操作。实际应用例如网页滚动加载

// 输入框输入节流
search.oninput = (function(){
let flag=true
return ()=>{
    if(flag) {
        setTimeout(()=>{
            console.log('发送了ajax请求');
            flag = true
        },500)
    }
    flag = false
}
})()

二:this和对象原型

2.1 什么是this

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数作用域中。

2.2 this指向

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

2.3 绑定规则

2.3.1 默认绑定

2.4 优先级

小结

  1. 普通函数的调用,this指向的是window
  2. 对象方法的调用,this指的是该对象,且是最近的对象
  3. 构造函数的调用,this指的是实例化的新对象
  4. apply和call调用,this指向参数中的对象
  5. 匿名函数的调用,this指向的是全局对象window
  6. 定时器中的调用,this指向的是全局变量window
  7. 箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象
  8. onclick和addEventerListener是指向绑定事件的元素(ev.currentTarget)

三:类型和语法

四:异步和性能