学习笔记整理,如果有帮到你万分荣幸(参考部分掘金大佬文章,如果侵权联系删除)
JS基础
作用域和作用域链
- 作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
- 作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。
- JS采用的是词法作用域
- **书写的过程中(例子中也就是在函数定义的时候,块作用域同理是在代码块定义的时候),根据你把它写在哪个位置来决定的。**像这样划分出来的作用域,遵循的就是词法作用域模型。
- 词法作用域和动态作用域的区别其实在于划分作用域的时机
- 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
- 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸
- 例子
-
var name = 'xiuyan'; function showName() { console.log(name); } function changeName() { var name = 'BigBear'; showName(); } changeName(); //xiuyan
-
JS内存管理
- 基本类型和引用类型
- JS 中的数据类型,整体上来说只有两类:基本类型(值类型)和引用类型
- 其中基本类型包括:Sting、Number、Boolean、null、undefined、Symbol。这类型的数据最明显的特征是大小固定、体积轻量、相对简单,它们被放在 JS 的栈内存里存储。
- 而排除掉基本类型,剩下的数据类型就是引用类型,比如 Object、Array、Function 等等等等。这类数据比较复杂、占用空间较大、且大小不定,它们被放在 JS 的堆内存里存储。
- JS 中的数据类型,整体上来说只有两类:基本类型(值类型)和引用类型
- 垃圾回收机制
- 每隔一段时间,JS 的垃圾收集器就会对变量做 “巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占用的内存空间给释放掉,这个过程叫做垃圾回收。
- 垃圾回收算法有两种 —— 引用计数法和标记清除法
-
引用计数法(已经淘汰)
- 在引用计数法的机制下,内存中的每一个值都会对应一个引用计数。当垃圾收集器感知到某个值的引用计数为 0 时,就判断它 “没用” 了,随即这块内存就会被释放。
-
标记清楚法
-
闭包 (关联考点 作用域,作用域链)
1.闭包是什么
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
闭包 = 函数 + 函数能够访问的自由变量。
2.闭包自由变量查询
自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
3.闭包缺点
会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏
4.闭包运用
- 能够访问函数定义时所在的词法作用域(阻止其被回收)
- 私有化变量
- 模拟块级作用域
- 创建模块
5.闭包形式
函数作为参数被传递:
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 100
函数作为返回值被返回:
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
6.真题
- 定时器循环问题
var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
this
- this指向谁(this 指向调用它所在方法的那个对象)
- 说得更通俗点, 谁调的函数,this 就归谁。当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined)
- this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反
- “秒杀” 技巧 —— 特殊情境下的 this 指向
- 在三种特殊情境下,this 会 100% 指向 window:
- 立即执行函数(IIFE)
- setTimeout 中传入的函数
- setInterval 中传入的函数
- 在三种特殊情境下,this 会 100% 指向 window:
- 箭头函数
- 箭头函数中的 this 比较特别,它和严格模式、非严格模式啥的都没关系。它和闭包很相似,都是认“死理”—— 认“词法作用域”的家伙。所以说箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定(和咱们普通函数的 this 规则恰恰相反~)换句话说就是包裹箭头函数的第一个普通函数的This
- 改变this指向
- 箭头函数
- bind,call,apply
执行上下文与调用栈
- 执行上下文是什么?
- 全局上下文 —— 全局代码所处的环境,不在函数中的代码都在全局执行上下文中
- 全局上下文的组成与创建
- 创建阶段 —— 执行上下文的初始化状态,此时一行代码都还没有执行,只是做了一些准备工作
- 创建全局对象(Window 有了)
- 创建 this ,并让它指向全局对象
- 给变量和函数安排内存空间
- 默认给变量赋值为 undefined;将函数声明放入内存
- 创建作用域链
- 执行阶段 —— 逐行执行脚本里的代码
- 创建阶段 —— 执行上下文的初始化状态,此时一行代码都还没有执行,只是做了一些准备工作
- 全局上下文的组成与创建
- 函数上下文 —— 在函数调用时创建的上下文
- 函数上下文的创建和组成
- 创建的时机 —— 全局上下文在进入脚本之初就被创建,而函数上下文则是在函数调用时被创建
- 创建的频率 —— 全局上下文仅在代码刚开始被解释的时候创建一次;而函数上下文由脚本里函数调用的多少决定,理论上可以创建无数次
- 创建阶段的工作内容不完全相同 —— 函数上下文不会创建全局对象(Window),而是创建参数对象(arguments);创建出的 this 不再死死指向全局对象,而是取决于该函数是如何被调用的 —— 如果它被一个引用对象调用,那么 this 就指向这个对象;否则,this 的值会被设置为全局对象或者 undefined(在严格模式下)
- 函数上下文的创建和组成
- Eval 执行上下文 —— 运行 Eval 函数中的代码时所创建的环境,Eval 被前端诟病多年,时下对 Eval 感兴趣的人非常少了,面试官也普遍对它嗤之以鼻。大家答题时只需要说明 “我不用 Eval”,直接跳过这个东西就好了(还可以拉一波好感度,说明你是一个明辨是非的好孩子)。综上所述,Eval 执行上下文,不在我们本文的讨论范围内。
- 全局上下文 —— 全局代码所处的环境,不在函数中的代码都在全局执行上下文中
- 站在上下文角度,理解 “变量提升” 的本质
- 现在结合我们的上下文创建过程,你会知道,其实根本不存在任何的 “提升”,变量一直在原地。所谓的 “提升”,只是变量的创建过程(在上下文创建阶段完成)和真实赋值过程(在上下文执行阶段完成)的不同步带来的一种错觉。执行上下文在不同阶段完成的不同工作,才是 “变量提升 “的本质。
- 调用栈(执行上下文栈)
- 我们看到函数执行完毕后,其对应的执行上下文也随之消失了。这个消失的过程,我们叫它” 出栈 “—— 没错,在 JS 代码的执行过程中,引擎会为我们创建” 执行上下文栈 “(也叫调用栈)。因为函数上下文可以有许多个,我们不可能保留所有的上下文。当一个函数执行完毕,其对应的上下文必须让出之前所占用的资源。因此上下文的建立和销毁,就对应了一个” 入栈 “和” 出栈 “的操作。当我们调用一个函数的时候,就会把它的上下文推入调用栈里,执行完毕后出栈,随后再为新的函数进行入栈操作。
原型与原型链
原型关系
- 每个class(构造函数)都有显示原型prototype
- 每个实例都有隐式原型
__proto__ - 实例的
__proto__指向对应class的prototype
原型:
在 JavaScript 中,每个构造函数(class)都拥有一个 prototype 属性,它指向构造函数的原型对象(构造函数.prototype ),这个原型对象中有一个 construtor 属性指回构造函数;每个实例都有一个__proto__属性,当我们使用构造函数去创建实例时,实例的__proto__属性就会指向构造函数的原型对象。
原型链(会画图)
函数的原型链对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用__proto__一直指向Object的原型对象上,而Object原型对象用Object.prototype.__proto__= null表示原型链顶端。如此形成了js的原型链继承。同时所有的js对象都有Object的基本防范
例子
// 创建一个Dog构造函数
function Dog(name, age) {
this.name = name
this.age = age
}
Dog.prototype.eat = function() {
console.log('肉骨头真好吃')
}
//或者class模式(等价)
//ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。
//类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN
class Dog {
constructor(name ,age) {
this.name = name
this.age = age
}
eat() {
console.log('肉骨头真好吃')
}
}
// 使用Dog构造函数创建dog实例
const dog = new Dog('旺财', 3)
- 关系图
- 构建函数拥有原型对象(Dog.prototype)
- 构造函数原型的construction(Dog.prototype.construction)指回构造函数(Dog)
- 实例对象(dog)的显示原型
_proto指向构造函数原型对象(Dog.prototype)
- 原型链(图)
new的实现
- 首先创建了一个新的空对象
- 根据原型链,设置空对象的
__proto__为构造函数的 prototype 。 - 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
- 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function myNew(context) {
const obj = new Object();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
}
异步
-
为什么需要异步?
- 因为js是单线程语言,遇到等待(网络请求,定时任务)不能卡住
- 解决单线程等待问题
-
Promise
- 解决了回调地狱的问题
- Promise 对象是一个代理对象。它接受你传入的 executor(执行器)作为入参,允许你把异步任务的成功和失败分别绑定到对应的处理方法上去。一个 Promise 实例有三种状态:
- pending 状态,表示进行中。这是 Promise 实例创建后的一个初始态;
- resolved 状态,表示成功完成。这是我们在执行器中调用 resolve 后,达成的状态;
- rejected 状态,表示操作失败、被拒绝。这是我们在执行器中调用 reject后,达成的状态;
- 当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的
- Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
-
Promise.resolve(1) .then(res => { console.log(res) // => 1 return 2 // 包装成 Promise.resolve(2) }) .then(res => { console.log(res) // => 2 })
-
-
async 及 await 的特点,它们的优点和缺点分别是什么?await 原理是什么?
- 一个函数如果加上 async,那么该函数就会返回一个 Promise
async function test() { return "1" } console.log(test()) // -> Promise {<resolved>: "1"}