JS知识点复习
2021-06-11 16:42:20 0 举报
AI智能生成
Javascript 知识点整理,作用域、调用栈、this解析、原型、继承、深浅拷贝
作者其他创作
大纲/内容
高阶函数
满足以下两个特点之一的函数
接收一个或多个函数作为参数。
输出一个函数
JS 中高阶函数
map
filter
reduce
柯里化(部分求值)
只传递一部分参数调用它,让它返回一个函数去处理剩下的参数。
应用
延迟计算
延迟计算例子
bind 函数
bind 改变函数的执行上下文,本身并不执行,本质也是延时计算。
动态创建函数
添加监听函数 addEvent : 兼容 IE
惰性函数
参数复用
const toStr = Function.prototype.call.bind(Object.prototype.toString);
toStr([1,2,3]) // [object Array]
实现原理
用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
防抖节流
防抖 debounce
原理:设置定时器,每次都用都重置定时器,只有定时器计时结束后才触发函数执行。
节流 throttle
某个函数在一定时间间隔内只执行一次。例如:3秒内无视后来产生的函数调用请求。
实现方式
时间戳:通过判断两个时间戳之间的差,决定是否执行。
定时器:存在定时器不执行,知道触发并且 handler 被清除,才重新设置定时器
手撕代码
call & apply & bind 实现
bind实现
call&apply实现
模拟实现 Object.assign
手写实现深拷贝?
模拟实现 instanceof 操作?
模拟实现 new 操作?(参考 new 绑定)
常见面试题
null 和 undefined 本质区别?
null
全局对象赋值 null,相当于变量指针以及值都清空。垃圾回收会回收全局变量为 null 的对象。
对象属性赋值 null,相当于属性分配空的内存,值为 null。
null 可以存放在 JSON 中,undefined 不可以。
undefined
全局对象赋值 undefined ,相当于对象的值清空,对象依然存在。
表示对象被声明,但值为空
undefined 做参数传递时,不会覆盖参数默认值。null 会覆盖参数默认值。
instanceof 原理及其实现
检测 constructor.prototype 是否存在于参数 object 的原型上。
原理:通过 __proto__ 一层层查找,如果 constructor.prototype 相等返回 true。
调用堆栈
执行上下文(EC)
1. 创建阶段
1. This Binding
默认绑定、隐式绑定、显式绑定、new绑定、箭头函数
2. LexicalEnvironment 词法环境创建
组成部分
环境记录:存储变量和函数声明
对外部环境的引用:访问外部的词法环境
类型
全局环境
环境记录:window 对象,全局方法和属性,其他自定义全局变量,this 执行全局对象。
外部引用:null
函数环境
环境记录:arguments 对象,函数中定义的变量
外部引用:全局环境或其他函数环境。
3. VariableEnvironment 变量环境创建(特殊的词法环境)
词法 vs 变量:
词法:存储函数声明和变量(let 和 const)绑定
变量:存储变量(var)绑定
特殊的词法环境:具有词法环境的所有属性。
变量提升原因?
在执行上下文创建阶段,变量和函数声明会被存储在环境中,这就是变量提升。
var 声明变量的变量被设置为:undefined, let / const 声明的变量被设置为未初始化。
在声明前访问 var 声明变量为 undefined;访问 let/const 声明变量提示引用错误的原因。
2. 执行阶段
对所有的变量分配和执行。在 let 变量无法找到值时,会被设置为 undefined。
执行栈(ECS)
JS 是单线程,在执行是会创建很多的执行上下文(EC),为了管理 EC 的执行顺序引入了执行栈 (ECS)。
变量对象(Variable Object)
变量对象(VO)
规范上或者JS引擎层面的,不能再 JS 环境中直接访问
活动对象 (AO)
进入执行上下文后,变量被激活(活动变量AO),可以访问其属性。
内存管理
堆 vs 栈
栈
存储基本类型
堆
存储引用类型
内存回收
标记清除
不再使用的对象被定义为:无法到达的对象。从根部出发可以达到的对象保留,无法到达的对象被回收。
引用计数
循环引用问题,两个对象互相引用,导致系统无法进行回收内存泄露
常见的内存泄露
意外的全局变量
严格模式可以避免 `use strict`,例如使用 `var` 定义变量会自动添加到全局。
被遗忘的计时器和回调函数
setInterval
脱离 DOM 的引用
对象中引用 DOM 节点,即使 DOM 树销毁,节点也不会回收。
闭包
闭包会捕获父级作用域的变量。
作用域闭包
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
函数
访问其他作用域变量
访问函数以外的变量
外部函数已返回,闭包仍能访问外部函数定义的变量。
可以更新外部变量
作用域链
闭包和作用域链有密切的关系
访问变量时,先从当前环境作用域中查找,没找到会去父级作用域中查找,知道找到或者到达顶端全局对象结束,这就是作用域链。
作用域链 vs 原型链
作用域链中找不到 变量 会抛出异常 ReferenceError。
对象属性在原型链中无法找到,会返回 undefined。
闭包是如何访问到外部变量的?
内部函数执行上下文中维护一个作用域链,会指向父级作用域。
作用域链为数组,当前作用域在最前面,依次向后排列,末尾是全局作用域。
即使当前作用域的父级函数已经销毁,但是当前作用域中还引用这父级函数作用域,所以可以访问外部变量。
闭包应用
实现私有变量
解决变量提升引起的 for + var 问题
this 全面解析
绑定规则
1. 默认绑定
独立函数调用,在无法应用其他规则时的默认规则,this 指向全局对象。
严格模式下,this 指向 undefined 而不是全局对象。在严格模式下调用函数不影响默认绑定。
2. 隐式绑定
函数引用有上下文对象时,会把 this 绑定到这个上下文对象上。例如:obj.bar()
隐式丢失:默认绑定在一些情况下会丢失,从而应用默认绑定把 this 绑定到全局对象或者 undefined 上。
输出:global var
虽然 bar 是 obj.foo 的一个引用,实际上引用的是 foo 函数本身。
执行 bar() 就是普通的函数调用,应用默认绑定。
参数传递是一种隐式赋值。
回调函数会丢失 this 绑定,例如:setTimeout
3. 显式绑定
通过 call() 和 apply() 方法,第一个参数是对象,调用时将其绑定到 this。
解决绑定丢失问题
硬绑定
创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。
硬绑定后,无法再改变 this 的指向。
ES6 内置的 bind 函数,返回一个硬绑定的新函数 (参考 bind 代码实现)
API 调用的 “上下文”
例如:forEach(callback, thisArgs) :thisArgs 是可选的 上下文
内置函数提供一个可选参数,被称为“上下文”,作用类似于 bind ,确保函数回调指定this.
这些函数的本质:通过 call 和 apply 实现显示绑定
4. new 绑定
构造函数
使用 new 操作符调用的“普通”函数被称为构造函数。
实际上不存在所谓的“构造函数”,只有对普通函数的 “构造调用”。
包含内置对象函数(如:Number)在内的所有函数都可以通过 new 进行调用,被称为构造函数调用。
new 调用过程
创建一个新空对象
这个新对象进行 [[prototype]] 连接。
将新对象绑定到函数调用的 this
函数没有返回值,直接返回新对象,否则返回函数返回值。
代码实现:__proto__ 使用 Object.setPrototypeOf() 代替
执行结果对比
5. 箭头函数
根据外层(全局或函数)作用域(词法作用域)来决定 this 指向。
特点
箭头函数不绑定 this, 其中的 this 相当于普通变量。
箭头函数中 this 寻值和普通函数一样,在作用域中逐级查找。
箭头函数的 this 无法通过 call、apply 、bind 直接修改(可以间接修改)。
改变作用域中 this 的指向可以改变箭头函数的 this (间接修改)。
题目
题目一
题解
题目二
题解
题目一 vs 题目二 区别?
new 操作,会创建构造函数作用域,this 会指向它。
call & apply 原理和作用
两者都可以指定 this 调用某个函数,区别前者接受参数列表,后者接受参数数组。
使用场景
合并两个数组 apply,第二个数组长度限制在 65535。可以通过数组切块解决这个问题。
获取数组中的最大值
验证是否是数组
类数组使用数组方法
调用父构造函数实现继承(参考八种继承方案)
......
bind原理
bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。
bind() 创建一个新函数,第一个参数作为 this,后面的参数以及新函数传入的参数按顺序传给原函数。
bind 返回的新函数可以进行 new 操作,相当于把原函数当做构造函数,此时提供的 this 会被忽略
深拷贝 vs 浅拷贝
浅拷贝:拷贝基本类型值和引用类型的地址。(第一层)
Object.assign
展开运算符...
数组的 slice 、concat 等方法
深拷贝:拷贝所有的属性,重新分配内存,两者会不影响。
JSON 的 parse 和 stringify 实现深化拷贝。
缺点:忽略 undefined、symbol、不能序列化、无法解决循环引用的对象、无法处理 Date、不能处理正则等等。
undefined/symbol/函数 会被忽略。
循环应用会报错
无法正确转换 new Date(),使用时间戳代替。
Object.assign 原理(浅拷贝)
将所有可枚举的属性值从一个或多个源对象赋值到目标对象,然后返回目标对象。
Object.assign(target, ...sources)
target 是目标对象,sources 是源对象一个或多个,返回修改后的目标对象 target。
相同的属性名,源对象会覆盖目标对象,后面的源对象会覆盖前面的源对象的属性。
特点
浅复制:基本类型的值,引用类型的引用地址。
可以复制 Symbol,以及 undefined 、null 等对象属性值。
如何实现深拷贝?(参考 手撕代码)
浅拷贝 + 递归
参数验证,注意 typeof null 返回 object 问题。(类型校验)
兼容数组返回 []。使用 typeof 进行对象类型判断,然后使用 isArray 判断是否初始化为 []。(数组初始化)
处理循环引用,使用 Hash 表记录已经存在过的对象,当发现已经存在过直接查表返回。(哈希表(数组)解决循环引用)
WeakMap
处理 Symbol (处理 Symbol 属性)
检验 Symbol 类型
Object.getOwnPropertySymbols(...) :查找对象符号属性,返回一个数组。
Reflect.ownKeys(...):目标对象属性键组成的数组。
方法一:查找并遍历 Symbol 属性,再处理正常情况。
方法二:获取目标对象自身所有的属性键,包括 Symbol。缺点不能拷贝原型链上的数据。for...in 可以深拷贝原型链上属性。
递归爆栈问题
尾递归
使用循环代替递归 :栈/队列 + 循环
原型 Prototype
构造函数
什么是构造函数?
constructor 返回创建对象时构造函数的引用。属性值是对函数本身的引用。
构造函数本身是一个函数,与普通函数没有区别。使用 new 生成实例的函数就是构造函数,直接调用就是普通函数。
Symbol 是构造函数吗?
基本数据类型,生成实例直接使用 `Symbol()` 即可。
constructor 属性只读吗?
引用类型:constructor 是可以修改的(如:原型链继承)
基本类型:constructor 不可以修改
new 原理及实现(参考 this 全面解析 new 绑定)
1. 创建一个对象
2. 获取构造函数,同时删除 arguments 第一个参数
3. 链接到原型,obj 可以访问构造函数原型中的属性
4. 绑定 this 实现继承,obj 可以访问到构造函数中的属性
5. 优先返回构造函数返回的对象
原型
prototype
每个对象都有一个原型,对象以原型为模板,从原型上继承方法和属性。
构造函数 Parent 有一个指向原型的指针,原型 Parent.prototype 有一个指向构造函数的指针 Parent.prototype.constructor。循环引用
__proto__
每个实例上都有一个 __proto__ 属性,访问器属性。
ES6 中被标准化,性能问题不推荐使用。推荐使用 Object.getPrototypeOf() 和 Object.setPrototypeOf()
原型链
每个对象都有一个原型,通过 __proto__ 指向原型,从中继承属性和方法。同时原型也有原型,一层层最终指向 null 。
原型链:继承依赖 __proto__ 而不是 Prototype。
鸡蛋问题
Object.prototype
原型的顶端(忽略 null 的情况下)
__proto__ 属性指向 null
Function.prototype
Function 的原型对象
没有 prototype 属性,
__proto__ 指向 Object.prototype
function Object
Object作为构造函数时, __proto__ 指向 Function.prototype
Function.prototype 的 __proto__ 属性指向 Object.prototype 属性
原型链
字面量对象 {}
o ---> Object.prototype --> null
数组字面量 []
a --> Array.prototype --> Object.prototype --> null
function foo() {} 创建对象
f --> Function.prototype --> Object.prototype --> null
new 创建对象
obj --> Foo.prototype --> Object.prototype --> null
function Function
注意:Function.__proto__ 属性指向 Funtion.prototype?
鸡蛋问题:Object 构造函数继承了 Function.prototype,而 Function 构造函数继承了 Object.prototype
Function.prototype 属性和 Function.__proto__ 属性都指向了 Function.prototype 对象。
Function 是不是有 Function 构造函数创建的对象?
是: Function instanceof Function 为 true
否: Function 是内置对象,指向只是为了保证原型链的完整性。
继承方案
原型链继承
本质:重写原型对象,指定为新的类型的实例。
父类Person 和 子类Student,通过原型链继承需要设置 Student.prototype = new Person()
instanceof 原理就是一层层查找 __proto__,如果 和 constructor.prototype 相同返回 true。
缺点
引用类型会被多个实例所共享,数据被篡改。例如:数组(引用类型被共享)
子类原型上的 constructor 方法被重写,Student.prototype.constructor != Student (原型constructor被重写)
必须在重置原型后向子类添加属性和方法,否则会被覆盖丢失(原型覆盖问题)。
无法向父类的构造函数传参(无法传参)
借用构造函数继承
本质:使用父类的构造函数增强子类实例,等同于复制父类的实例给子类。
子类 Student 中执行父类构造函数 SuperType.call(this),子类的每个实例都会将父类的属性复制一份。
缺点
只能继承父类的实例属性和方法,无法继承原型的。
无法实现复用,每个子类实例都存在父类的副本
组合继承
原型链 实现原型属性和方法的继承,借用构造函数 实现实例属性的继承。
缺点:调用两次父类的构造函数,子类的实例和原型上都存在相同的父类属性和方法 (调用两次父类构造函数)
原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。
缺点
原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
无法传递参数
寄生式继承
在原型式继承的基础上,增强对象,返回构造函数
缺点:同原型继承
寄生式组合模式
特点:
结合借用构造函数传递参数和寄生模式实现继承
只调用一个父类的构造函数,原型链保持不变
这是最成熟的方法,也是现在库实现的方法
代码实现
代码实现
继承关系图
ES6继承extends
extends继承的核心代码实现和上述的寄生组合式继承方式一样
0 条评论
下一页