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 可以被修改,这些规则要吃透需要稍微花的时间,以下是我之前整理的题,大家可以试试。
函数被正常执行时 foo(),this 指向啥?
globalThis
箭头函数的 this 指向啥?
父作用域。 箭头函数没有原型,访问this会得到就近外层函数的this。
this 可通过哪些方式被修改?
3种: bind、call、apply。 其中的优先级: new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.foo()) > 默认绑定(独立函数调用)。
箭头函数的 this 可被修改吗?
不可。 箭头函数获得的this在运行时被确定,不能修改。因为本身没有可绑定的 this。
class 箭头函数定义的方法 this 指向啥?
实例对象。 class中的方法如果是普通函数方法,该方法会绑定在构造函数的原型上。 但是如果方式是箭头函数方法,该方法会绑定在构造函数上。 通过上述方式调用class中的方法,无论是箭头函数方法还是普通函数方法,方法中的this都指向实例对象。
bind 过的函数,还可通过 call 和 apply 修改 this 吗?
不可。
函数作为对象成员调用时,比如 foo 对象中的 bar 方法 foo.bar() 中的 this 指向啥?
foo对象自己。
函数或其父作用域为严格模式时,this 指向啥?
undefined。 在严格模式下, 全局作用域中的this指向window对象。 全局作用域中的函数中的this等于undefined。 对象的函数中的this指向调用函数的对象实例。 构造函数中的this指向构造函数创建的对象实例。 在事件处理函数中,this指向触发事件的目标对象。