JavaScript 编程基础

# JavaScript 编程基础

# JS 的内存分为栈、堆、池

其中栈存放变量,堆存放对象,池存放常量,所以也叫常量池

#

栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。栈是一种先进后出的结构,为了获得栈底的元素,必须拿掉上面的元素。

#

一种排序过的树状结构,我们说的堆,通常指二叉堆,必须是完全二叉树(近似满二叉树)。堆的特点是根节点值是整个树中的最值(大顶堆/小顶堆)

堆经常被用来实现优先队列,这种结构优势是可以动态分配内存大小,缺点是运行时需动态分配内存,存取速度较慢

# 数据类型

# 基本数据类型

String、Number、Boolean、null、undefined、Symbol

保存在栈内存中,因为占用空间小、大小固定,频繁被使用

  • 产生闭包的变量会被存放在堆中,因为栈在上下文执行完毕后会被 GC,堆不会。
  • 由 JS 引擎逃逸分析出变量应该存在哪种结构中。

# 引用数据类型

Object、Array、Function...

存储在堆内存

  • 占据空间大、大小不确定
  • 引用数据类型在栈中存储了指针,指向堆内该实体的起始地址
  • Js 解释器寻找引用值时,会先检索栈中的地址,再获得堆中的实体

# NaN(Not-A-Number)

NaN 是一个 value, 这个 value 的 type 是 number。

但是跟普通的 type 是 number 的 value 不一样的是,NaN 代表 'Not a number' 这一意义。

# isNaN()

只要不是 number 就会返回 true。

isNaN('A String'); // true

isNaN(undefined); // true

isNaN({}); // true

# Number.isNaN()

只有 Number.isNaN(NaN)返回 true。

Number.isNaN(NaN); // true

Number.isNaN('A String'); // false

Number.isNaN(undefined); // false

Number.isNaN({}); // false

# 变量赋值

  • 基本数据类型直接拷贝一份

  • 引用数据类型相当于多了一个地址指针,实际指向同一个对象

# Js 创建对象

  • new 关键字执行构造函数
  • 对象字面量

# new 关键字做了什么

  • 在内存中创建一个空对象
  • 类的 prototype 赋值给对象内部的 prototype
  • 构造函数内的 this 被指向创建出来的新对象
  • 执行构造函数的内部代码
  • 如果构造函数没有返回对象,则默认返回创建出来的新对象

# this 绑定的优先级

由高到低:

  • new 绑定
  • 显式绑定(call(obj, args1, arg2...)、apply(obj, [args])、bind(obj, arg1, arg2, ...)非立即执行)
  • 隐式绑定(由对象发起的调用)
  • 默认绑定(独立函数调用)

# Js 作用域

# 作用域不等于执行上下文

JavaScript 属于解释型语言,他的执行分为解释和执行两个阶段

解释阶段

  • 词法分析
  • 语法分析
  • 作用域规则确定

执行阶段

  • 创建执行上下文
  • 执行函数
  • 垃圾回收

JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

作用域和执行上下文之间最大的区别是: 执行上下文(EC)在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

作用域决定了代码块中变量和方法的可见性。

# 作用域链

当前作用域下没有定义的变量,变成自由变量。自由变量会到父级作用域(*实际上是自由变量所在的函数创建时的作用域中)查找,一层一层向上,直到找到全局作用域还是没找到,就宣布放弃(undefined)。

var x = 10;
function fn() {
  console.log(x);
}
function show(f) {
  var x = 20(function () {
    f(); //10,而不是20
  })();
}
show(fn);

# 全局作用域

# 初始化全局对象

  • js 引擎在执行代码之前,会在堆内存中创建一个全局对象,Global Object(GO)
  • 所有作用域 都可以访问该对象
  • 里面会自带一些工具函数,date、setTimeout、Infinity 等等
  • 包含一个指向自己的 window 属性

# 执行上下文栈 ECS(调用栈)

  • js 引擎内部有一个 ECS,用于执行代码的调用栈。全局 EC 中,变量对象(VO)就是 GO

  • 全局的代码会先构建一个全局执行上下文(GEC)

  • GEC 会被压入 ECS 中执行

    • 步骤一:parse 转换成 AST 的过程中,将全局定义的变量、函数加到 GO 中,但是不会赋值。

      这就是变量的作用域提升

    • 步骤二:在代码的执行过程中,该赋值的赋值,该执行的执行

# 函数作用域

在执行一个函数时,会根据函数体创建一个函数执行上下文(FEC),压入 ECS。包含以下三部分

  • 在解析成 AST 时,会创建活跃对象(AO)

    AO 中包含形参、arguments、函数定义和指向函数的对象、变量

  • 作用域链:由 VO(在函数中就是 AO 对象)和父级 VO 组成,一层一层向外查找

# 块级作用域

  • ES5 中只有函数作用域、全局作用域。实际上 ES6 之后出现了块级作用域,可用 let、const 生成。而且前面提到的变量对象 VO 改名了,现在叫变量环境 VE

  • 两种创建方式

    • 在一个函数内部,function、箭头函数
    • 在一个花括号内部
  • 块级作用域中声明变量不会提升到代码块顶部

  • 禁止重复声明

  • 避免了 for 循环中使用延时函数产生闭包导致的 bug,绑定块级作用域形成父子作用域解决

# 云谦讲解的 JS 基础

# 🐍 作用域

作用域决定执行代码时的上下文,包括可以访问哪些变量、函数、对象等。

通常大家会接触到的有全局作用域、函数作用域、块级作用域、词法作用域、作用域链。

全局作用域只有一个,就是定义在最外面的那个。

函数会创建新的作用域,即函数作用域,每次执行函数,都会创建新的作用域。

函数作用域会在函数执行时被绑定到函数执行时的上下文。函数作用域里的变量只能在函数体内被访问到。

if、switch、while、for 等语句会创建块级作用域。

那块级作用域里的变量能在外部被访问吗?

这要看是用 var 还是 let/const 了。var 是函数作用域级,而 let 和 const 是块级作用域级。

JavaScirpt 处理作用域的方式是词法作用域,也叫静态作用域,与之相对的是动态作用域,很少有语言用,比如 Bash 就是动态作用域。

词法作用域决定了作用域在代码被 JavaScript 引擎编译时即会决定,而不是在代码被运行时决定。

最后,作用域链是什么?

执行上下文会为每个词法环境创建指向父作用域的引用,所以往里的作用域就能访问往外的作用域,而外面的不能访问里面的。

# 🐹 闭包

理解了作用域,闭包就很好理解了。

闭包是指嵌套函数里,子函数能访问父函数的作用域,就算父函数已经完成执行。

原因是子函数的执行上下文创建了指向父函数作用域的引用,所以子函数没结束,引用一直存在,父函数的作用域就一直存在。

# 🐢 Hoist-提升

作用域内的变量和函数会在编译阶段被放到内存里的过程叫 Hoist。

有个误解是大家认为 Hoist 是把他们提到作用域的前面,但其实并不是,var、let、const、function 都会被 hoist,只是有些不会被初始化,所以提前使用会报 ReferenceError。

var 会被初始化为 undefined,传统的函数定义 functon foo() {} 会被初始化,let 和 const 不会被初始化。

所以 var 定义的变量可以提前使用,但值是 undefined,传统函数定义可以正常提前使用,let 和 const 提前使用会报错。

注意,函数也可通过 var、let、const 的方式定义。

case 如下:

let foo = () => {};

a; // undefined
b; // Reference Error
c; // Reference Error
d; // function d() {}

var a = 1;
let b = 2;
const c = 3;
function d() {}

# 🦀 this 关键字

一句话概括:this 的指向是由所在函数的调用方式决定的。

函数被调用时,会创建执行上下文,这个上下文中存了很多信息,比如函数从哪里被调用、传入的参数等。

其中 this 关键字就是用来保存函数从哪里来。

不同场景下 this 的指向是不同的,并且 this 可以被修改,这些规则要吃透需要稍微花的时间,以下是我之前整理的题,大家可以试试。

  1. 函数被正常执行时 foo(),this 指向啥?

    globalThis
    
  2. 箭头函数的 this 指向啥?

    父作用域。
    箭头函数没有原型,访问this会得到就近外层函数的this。
    
  3. this 可通过哪些方式被修改?

    3种: bind、call、apply。
    其中的优先级:
    new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.foo()) > 默认绑定(独立函数调用)。
    
  4. 箭头函数的 this 可被修改吗?

    不可。
    箭头函数获得的this在运行时被确定,不能修改。因为本身没有可绑定的 this。
    
  5. class 箭头函数定义的方法 this 指向啥?

    实例对象。
    class中的方法如果是普通函数方法,该方法会绑定在构造函数的原型上。
    但是如果方式是箭头函数方法,该方法会绑定在构造函数上。
    通过上述方式调用class中的方法,无论是箭头函数方法还是普通函数方法,方法中的this都指向实例对象。
    

    class 中的箭头函数和普通函数的 this 指向 (opens new window)

  6. bind 过的函数,还可通过 call 和 apply 修改 this 吗?

    不可。
    
  7. 函数作为对象成员调用时,比如 foo 对象中的 bar 方法 foo.bar() 中的 this 指向啥?

    foo对象自己。
    
  8. 函数或其父作用域为严格模式时,this 指向啥?

    undefined。
    在严格模式下,
    全局作用域中的this指向window对象。
    全局作用域中的函数中的this等于undefined。
    对象的函数中的this指向调用函数的对象实例。
    构造函数中的this指向构造函数创建的对象实例。
    在事件处理函数中,this指向触发事件的目标对象。