Appearance
JavaScript 语言特性
执行上下文(EC)
即 js 代码的运行环境,由于 js 中可执行代码类型为 3 种,分别为全局代码、函数代码、eval 代码,所以对应的执行上下文的 3 种类型分别为全局执行上下文、函数执行上下文、eval 函数执行上下文,每当遇到可执行代码时就会创建一个对应类型的执行上下文压入执行栈中,对于每个执行上下文都会有 3 个重要的属性,分别是变量对象、作用域链、this 对象。
变量对象(VO/AO),是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明,对于全局上下文来说,变量对象初始化是全局对象,而对于函数上下文来说,函数在创建时作用域就确定了,此时会保存所有的父变量对象到内部属性
[[scope]]中,可以理解为[[scope]]就是所有父变量对象的层级链,当函数激活时,会将活动对象添加到作用域链顶端,用 arguments 创建活动对象,之后加入形参、函数声明、变量声明作用域链(scope),当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象,这样由多个执行上下文的变量对象组成链表就叫做作用域链
this,始终指向最后调用它的那个对象
执行栈
一个用于管理执行上下文的栈结构(后进先出)的容器,由于首先遇到的是全局代码,所以会向其中先压入全局执行上下文,而每当创建了一个执行上下文,就会将该执行上下文压入栈中,当函数执行完之后再将该执行上下文弹出,在整个应用程序结束的时候,执行栈才会被清空,所以在此之前,执行栈中始终有个全局执行上下文
作用域
作用域是一套规则,用于确定在何处以及如何查找变量(标识符) 函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)
作用域的工作模型
词法作用域(js 用的就是这种):就是定义在词法阶段的作用域,换句话说,词法作用域是由写代码时将变量和块级作用域写在哪里来决定的,因此词法分析器处理代码时会保持作用域的不变(大部分情况下是如此,但有欺骗词法作用域的方法,这里不做讨论)
动态作用域
闭包
闭包就是有权访问另一个函数作用域中的变量的函数,是基于词法作用域书写代码时产生的自然结果。
代码解析:
// 代码A
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 执行栈变化:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
// 代码B
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
// 执行栈变化:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
原型链
每个对象都有一个原型对象,通过proto指针指向上一个原型,并从中继承属性和方法,同时原型对象也能拥有原型,这样一层一层形成的链条就叫原型链。
js 中一切皆对象,函数也属于对象
所有对象都有
__proto__只有函数有
prototype所有函数的默认原型都是
Object的实例
原型、实例、构造函数之间的关系
构造函数的 prototype 指向原型,实例的proto指向原型,原型的 constructor 指向构造函数,实例和构造函数无直接联系,但实例的 constructor 会指向构造函数,实际上是沿着原型链找到原型,而原型上有 constructor 属性
一句话解释 0.1+0.2 为什么不等于 0.3?
计算机中的数字都是以二进制存储的,如果要计算 0.1 + 0.2 的结果,计算机会先把 0.1 和 0.2 分别转化成二进制,然后相加,最后再把相加得到的结果转为十进制,而 EcmaScrpt 规范定义 Number 的类型遵循了 IEEE754-2008 中的 64 位浮点数规则定义的小数后的有效位数至多为 52 位,所以会导致计算出现精度丢失问题
this指向问题和apply、call、bind的区别
this永远指向最后调用它的那个对象,而通过一些方法可以改变this的指向:
使用箭头函数 => 在箭头函数中的
this就是定义时所在的对象,而不是调用时所在的对象用_this = this
使用
apply、call、bind方法
调用call、apply、bind的必须是个函数,因为call、apply、bind都是挂在Function对象上的方法,只有函数才有这些方法。三个方法都第一个参数都是想让this指向的对象,其中apply、call的区别是后面传入的参数不同,apply后面传参的是数组,而call后面传的一或多个参数,而apply、call与bind的区别在于,apply、call是改变this指向后立即执行该函数,bind则是改变this指向后返回一个新函数,所以必须手动调用,同时后面传的一或多个参数
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
当thisArg为undefined/null时,非严格模式下,函数中的this指向window/global,严格模式下,函数中的this则为undefined
当thisArg为原始值(数字,字符串,布尔值)时this会指向该原始值的自动包装对象,如String、Number、Boolean
数据类型概念?区分(保存机制)?
概念:ECMAScritp 中有 2 种数据类型,分别为基本数据类型和复杂数据类型(Object),其中基本数据类型包括Undefined、Null、Boolean、Number、String、Symbal(实例唯一,不可改变),ES10 中还新增了 BigInt,除了基本数据类型,其他的都属于复杂数据类型(Object/引用类型),比如Object、Function、Date、Array、RegExp等
区分(保存机制):
基本数据类型不可变性。js 中,每一个变量在内存中都需要一个空间来存储,基本数据类型存储在栈中,由于栈中的内存空间的大小是固定的,那么注定了存储在栈中的变量就是不可变的,每个变量间互不影响,一些常见修改字符串的方法,比如slice()、substr()实际上是产生了一个新的字符串,并非修改原字符串,而比如 let str = 'abc';str+='6';,此时str为'abc6',但实际上是开辟了一块新的存储空间用于储存变量str,并非直接修改;
而对于引用类型,是不具有不可变性的,引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值;
比较:对于基本类型比较时会直接比较它们的值,如
let a = 'abc'
let b = 'abc'
a === b // true
而对于引用类型,比较时会比较它们的引用地址,即使两个变量在堆中存储的对象具有的属性值都是相等的,但只要被存储在了不同的存储空间,也是不等的(== 或 ===),如
let a = { name: 'abc' }
let b = { name: 'abc' }
a == b // false
a === b // false
let c = { name: 'abc' }
let d = c
c == d // true
c === d // true
EMCAStript 中所有函数的参数都是按值传递的
参数为基本类型时,由于不可变性,相当于复制该参数的一个副本,对该参数的修改不会影响到外部变量
参数为引用类型时,传递的是该引用类型保存在栈中的引用地址
流控制语句判断机制
流控制语句判断(如 if 语句)判断时,会自动执行Boolean()转型函数,Boolean()只有以下 6 个会转化为 false,其余转化为 true,假值分别为:null、undefined、''、NaN、0、false
运算符
- 对各种非 Number 类型运用数学运算符
-、*、/时,会先将非Number类型转换为Number类型,如:
1 - true // 0
1 - null // 1
1 * undefined // NaN
2 * ['5'] // 10
- 而对于
+时,当一侧为String,则优先将另一侧转为String;当一侧为Number,另一侧为基本类型时,则将基本类型转为Number;当一侧为Number,另一侧为引用类型时,会将Number和引用类型都转为String,如:
123 + '123' // '123123'
123 + null // 123
123 + true // 124
123 + {} // '123[object Object]'
{name: '123'} + 123 // '[object Object]123'
[] + 123 // '123' => 空数组转化为字符串是''
['abc','14'] + 123 // 'abc,14123'
比较
两个引用类型比较则直接比较是否指向同个对象(无论是==还是===)
==的隐氏转换:
NaN和其他任何类型比较永远返回false(包括和他自己),任何涉及NaN的操作都会返回NaNBoolean和其他任何类型比较,Boolean首先被转换为 Number 类型String和Number比较,先将String转换为Number类型引用类型和基本类型比较时,先调用
valueOf()得到基本类型值再按其他规则比较null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false
写一个对象 a,让 a == 1 && a == 2 && a == 3成立:
let a = {
value: [1, 2, 3],
valueOf: function () {
return this.value.shift()
}
}
a == 1 && a == 2 && a == 3 // true
javascript的执行机制
在不同的环境下,javascript的执行机制可能并不相同
javascript是单线程语言,原因是为了避免DOM渲染的冲突,即使是HTML5中出现的Web Worker允许在主线程之外创建后台进程的本意也是为了处理复杂或大量的计算逻辑,在当中也是不能访问DOM的,也无法通过任何方式影响页面的外观
javascript中代码任务分为同步任务和异步任务,从这个角度来说,代码由上至下执行,当遇到同步任务时会将同步任务推进执行栈中立即执行,当遇到异步任务时会将异步任务推进任务队列中,等执行栈中的任务完成后再执行队列中的异步代码,如此反复
同时,javascript代码不仅可以分为同步任务和异步任务,从另一个纬度还可分为宏任务和微任务,除了Process.nextTick > Promise.then/catch/finally > MutationObserver这几个微任务之外的都是宏任务,从这角度来说,每次单个宏任务执行完毕后,检查微任务队列是否为空,如果不为空的话,会按先入先出的规则依次执行微任务,直至为微任务队列为空,然后再执行下一个宏任务,如此反复
判断法则:同步代码立即执行,异步代码判断是宏任务还是微任务,分别推入宏任务队列和微任务队列,每次执行宏任务时先判断微任务队列是否为空,不为空则依次执行微任务直至清空,之后再执行宏任务队列,如此反复