- 数据类型
String、Number、Boolean、Null、Undefined、Symbol、BigInt、Object
- 堆、栈
两者都是存放数据的地方。
栈(stack)是自动分配的内存空间,它存放基本类型的值和引用类型的内存地址。
堆(heap)是动态分配的内存空间,它存放引用类型的值。
JavaScript 不允许直接操作堆空间的对象,在操作对象时,实际操作是对象的引用,
而存放在栈空间中的内存地址就起到指向的作用,通过内存地址找到堆空间中的对应引用类型的值。
- 隐式类型转换
JavaScript 作为一个弱类型语言,因使用灵活的原因,在一些场景中会对类型进行自动转换。
常见隐式类型转换场景有3种:运算、取反、比较
1.运算
运算的隐式类型转换会将运算的成员转换为 number 类型。
基本类型转换:
true + false // 1
null + 10 // 10
false + 20 // 20
undefined + 30 // NaN
1 + '2' // "12"
NaN + '' // "NaN"
undefined + '' // "undefined"
null + '' // "null"
'' - 3 // -3
1)null、false、'' 转换 number 类型都是 0
2)undefined 转换 number 类型是 NaN,所以 undefined 和其他基本类型运算都会输出 NaN
3)字符串在加法运算(其实是字符串拼接)中很强势,
和任何类型相加都会输出字符串(symbol除外),即使是 NaN、undefined。
其他运算则正常转为 number 进行运算。
引用类型转换:
[1] + 10 // "110"
[] + 20 // "20"
[1,2] + 20 // "1,220"
[20] - 10 // 10
[1,2] - 10 // NaN
({}) + 10 // "[object Object]10"
({}) - 10 // NaN
1)引用类型运算时,会默认调用 toString 先转换为 string
2)同上结论,除了加法都会输出字符串外,其他情况都是先转 string 再转 number
解析引用类型转换过程
[1,2] + 20
// 过程:
[1,2].toString() // '1,2'
'1,2' + 20 // '1,220'
[20] - 10
// 过程
[20].toString() // '20'
Number('20') // 20
20 - 10 // 10
2.取反
取反的隐式类型转换会将运算的成员转换为 boolean 类型。
这个隐式类型转换比较简单,就是将值转为布尔值再取反:
![] // false
!{} // false
!false // true
通常为了快速获得一个值的布尔值类型,可以取反两次:
!![] // true
!!0 // false
3.比较
比较分为 严格比较=== 和 非严格比较==,由于 === 会比较类型,不会进行类型转换。这里只讨论 ==
比较的隐式类型转换基本会将运算的成员转换为 number 类型。
undefined == null // true
'' == 0 // true
true == 1 // true
'1' == true // true
[1] == '1' // true
[1,2] == '1,2' // true
({}) == '[object Object]' // true
1)undefined 等于 null
2)字符串、布尔值、null比较时,都会转 number
3)引用类型在隐式转换时会先转成 string 比较,如果不相等然再转成 number 比较
- 预编译
预编译发生在 JavaScript 代码执行前,对代码进行语法分析和代码生成,
初始化的创建并存储变量,为执行代码做好准备。
预编译过程:
1)创建GO/AO对象(GO是全局对象,AO是活动对象)
2)将形参和变量声明赋值为 undefined
3)实参形参相统一
4)函数声明提升(将变量赋值为函数体)
例子:
function foo(x, y) {
console.log(x)
var x = 10
console.log(x)
function x(){}
console.log(x)
}
foo(20, 30)
// 1. 创建AO对象
AO {}
// 2. 寻找形参和变量声明赋值为 undefined
AO {
x: undefined
y: undefined
}
// 3. 实参形参相统一
AO {
x: 20
y: 30
}
// 4. 函数声明提升
AO {
x: function x(){}
y: 30
}
编译结束后代码开始执行,第一个 x 从 AO 中取值,输出是函数x;x 被赋值为 10,第二个 x 输出 10;
函数x 已被声明提升,此处不会再赋值 x,第三个 x 输出 10。
- 作用域
作用域能保证对有权访问的所有变量和函数的有序访问,是代码在运行期间查找变量的一种规则。
- 函数作用域
函数在运行时会创建属于自己的作用域,将内部的变量和函数定义“隐藏”起来,
外部作用域无法访问包装函数内部的任何内容。
- 块级作用域
在ES6之前创建块级作用域,可以使用 with 或 try/catch。而在ES6引入 let 关键字后,
让块级作用域声明变得更简单。let 关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)
{
let num = 10
}
console.log(num) // ReferenceError: num is not defined
- 参数作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。
等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
let x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,
默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。
let x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
x // 1
y 的默认是一个匿名函数,匿名函数内的x指向同一个作用域的第一个参数x。
函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的。
y函数执行对参数x重新赋值,最后输出的就是2,而外层的全局变量x依然不受影响。
- 闭包
闭包的本质就是作用域问题。当函数可以记住并访问所在作用域,
且该函数在所处作用域之外被调用时,就会产生闭包。
简单点说,一个函数内引用着所在作用域的变量,
并且它被保存到其他作用域执行,引用变量的作用域并没有消失,
而是跟着这个函数。当这个函数执行时,就可以通过作用域链查找到变量。
let bar
function foo() {
let a = 10
// 函数被保存到了外部
bar = function () {
// 引用着不是当前作用域的变量a
console.log(a)
}
}
foo()
// bar函数不是在本身所处的作用域执行
bar() // 10
优点:私有变量或方法、缓存
缺点:闭包让作用域链得不到释放,会导致内存泄漏
- 原型链
JavaScript 中的对象有一个特殊的内置属性 prototype(原型),它是对于其他对象的引用。
当查找一个变量时,会优先在本身的对象上查找,如果找不到就会去该对象的 prototype 上查找,
以此类推,最终以 Object.prototype 为终点。多个 prototype 连接在一起被称为原型链。
- 原型继承
原型继承的方法有很多种,这里不会全部提及,只记录两种常用的方法。
1.圣杯模式
function inherit(Target, Origin){
function F() {};
F.prototype = Origin.prototype;
Target.prototype = new F();
// 还原 constuctor
Target.prototype.constuctor = Target;
// 记录继承自谁
Target.prototype.uber = Origin.prototype;
}
圣杯模式的好处在于,使用中间对象隔离,子级添加属性时,都会加在这个对象里面,
不会对父级产生影响。而查找属性是沿着 __proto__ 查找,可以顺利查找到父级的属性,实现继承。
使用:
function Person() {
this.name = 'people'
}
Person.prototype.sayName = function () { console.log(this.name) }
function Child() {
this.name = 'child'
}
inherit(Child, Person)
Child.prototype.age = 18
let child = new Child()
2.ES6 Class
class Person {
constructor() {
this.name = 'people'
}
sayName() {
console.log(this.name)
}
}
class Child extends Person {
constructor() {
super()
this.name = 'child'
}
}
Child.prototype.age = 18
let child = new Child()
Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,
要清晰和方便很多。
- 基本包装类型
let str = 'hello'
str.split('')
基本类型按道理说是没有属性和方法,但是在实际操作时,
我们却能从基本类型调用方法,就像一个字符串能调用 split 方法。
为了方便操作基本类型值,每当读取一个基本类型值的时候,
后台会创建一个对应的基本包装类型的对象,
从而让我们能够调用方法来操作这些数据。大概过程如下:
1)创建String类型的实例
2)在实例上调用指定的方法
3)销毁这个实例
let str = new String('hello')
str.split('')
str = null
- this
this是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
我理解的this是函数的调用者对象,当在函数内使用this,可以访问到调用者对象上的属性和方法。
this绑定的四种情况:
new 绑定。new实例化
显示绑定。call、apply、bind手动更改指向
隐式绑定。由上下文对象调用,如 obj.fn(),this 指向 obj
默认绑定。默认绑定全局对象,在严格模式下会绑定到undefined
优先级new绑定最高,最后到默认绑定。
- new的过程
1)创建一个空对象
2)设置原型,将对象的 __proto__ 指向构造函数的 prototype
3)构造函数中的 this 执行对象,并执行构造函数,为空对象添加属性和方法
4)返回实例对象
注意点:构造函数内出现return,如果返回基本类型,则提前结束构造过程,
返回实例对象;如果返回引用类型,则返回该引用类型。
// 返回基本类型
function Foo(){
this.name = 'Joe'
return 123
this.age = 20
}
new Foo() // Foo {name: "Joe"}
// 返回引用类型
function Foo(){
this.name = 'Joe'
return [123]
this.age = 20
}
new Foo() // [123]
- call、apply、bind
三者作用都是改变this指向的。
call 和 apply 改变 this 指向并调用函数,它们两者区别就是传参形式不同,
前者的参数是逐个传入,后者传入数组类型的参数列表。
bind 改变 this 并返回一个函数引用,bind 多次调用是无效的,
它改变的 this 指向只会以第一次调用为准。
1)手写call
Function.prototype.mycall = function () {
if(typeof this !== 'function'){
throw 'caller must be a function'
}
let othis = arguments[0] || window
othis._fn = this
let arg = [...arguments].slice(1)
let res = othis._fn(...arg)
Reflect.deleteProperty(othis, '_fn') //删除_fn属性
return res
}
apply 实现同理,修改传参形式即可
2)手写bind
Function.prototype.mybind = function (oThis) {
if(typeof this != 'function'){
throw 'caller must be a function'
}
let fThis = this
//Array.prototype.slice.call 将类数组转为数组
let arg = Array.prototype.slice.call(arguments,1)
let NOP = function(){}
let fBound = function(){
let arg_ = Array.prototype.slice.call(arguments)
// new 绑定等级高于显式绑定
// 作为构造函数调用时,保留指向不做修改
// 使用 instanceof 判断是否为构造函数调用
return fThis.apply(this instanceof fBound ? this : oThis, arg.concat(arg_))
}
// 维护原型
if(this.prototype){
NOP.prototype = this.prototype
fBound.prototype = new NOP()
}
return fBound
}
- 对ES6语法的了解
常用:let、const、扩展运算符、模板字符串、对象解构、箭头函数、默认参数、Promise
数据结构:Set、Map、Symbol
其他:Proxy、Reflect
Set、Map、WeakSet、WeakMap
Set:
1)成员的值都是唯一的,没有重复的值,类似于数组
2)可以遍历
WeakSet:
1)成员必须为引用类型
2)成员都是弱引用,可以被垃圾回收。成员所指向的外部引用被回收后,该成员也可以被回收
3)不能遍历
Map:
1)键值对的集合,键值可以是任意类型
2)可以遍历
WeakMap:
1)只接受引用类型作为键名
2)键名是弱引用,键值可以是任意值,可以被垃圾回收。键名所指向的外部引用被回收后,
对应键名也可以被回收
3)不能遍历
- 箭头函数和普通函数的区别
1)箭头函数的this指向在编写代码时就已经确定,即箭头函数本身所在的作用域;
普通函数在调用时确定this。
2)箭头函数没有arguments
3)箭头函数没有prototype属性
- Promise
Promise 是ES6中新增的异步编程解决方案,避免回调地狱问题。Promise 对象是通过状态的改变来实现
通过同步的流程来表示异步的操作, 只要状态发生改变就会自动触发对应的函数。
Promise对象有三种状态,分别是:
pending: 默认状态,只要没有告诉 promise 任务是成功还是失败就是 pending 状态
fulfilled: 只要调用 resolve 函数, 状态就会变为fulfilled, 表示操作成功
rejected: 只要调用 rejected 函数, 状态就会变为 rejected, 表示操作失败
状态一旦改变既不可逆,可以通过函数来监听 Promise 状态的变化,
成功执行 then 函数的回调,失败执行 catch 函数的回调
- 浅拷贝
浅拷贝是值的复制,对于对象是内存地址的复制,目标对象的引用和源对象的引用指向的是同一块内存空间。
如果其中一个对象改变,就会影响到另一个对象。
常用浅拷贝的方法:
1.Array.prototype.slice
let arr = [{a:1}, {b:2}]
let newArr = arr1.slice()
2.扩展运算符
let newArr = [...arr1]
- 深拷贝
深拷贝是将一个对象从内存中完整的拷贝一份出来,对象与对象间不会共享内存,
而是在堆内存中新开辟一个空间去存储,所以修改新对象不会影响原对象。
常用的深拷贝方法:
1.JSON.parse(JSON.stringify())
JSON.parse(JSON.stringify(obj))
2.手写深拷贝
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
const type = Object.prototype.toString.call(obj).slice(8, -1)
let strategy = {
Date: (obj) => new Date(obj),
RegExp: (obj) => new RegExp(obj),
Array: clone,
Object: clone
}
function clone(obj){
// 防止循环引用,导致栈溢出,相同引用的对象直接返回
if (map.get(obj)) return map.get(obj);
let target = new obj.constructor();
map.set(obj, target);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
target[key] = deepClone(obj[key], map);
}
}
return target;
}
return strategy[type] && strategy[type](obj)
}
- 事件委托
事件委托也叫做事件代理,是一种dom事件优化的手段。事件委托利用事件冒泡的机制,
只指定一个事件处理程序,就可以管理某一类型的所有事件。
假设有个列表,其中每个子元素都会有个点击事件。当子元素变多时,
事件绑定占用的内存将会成线性增加,这时候就可以使用事件委托来优化这种场景。
代理的事件通常会绑定到父元素上,而不必为每个子元素都添加事件。
<ul @click="clickHandler">
<li class="item">1</li>
<li class="item">2</li>
<li class="item">3</li>
</ul>
clickHandler(e) {
// 点击获取的子元素
let target = e.target
// 输出子元素内容
consoel.log(target.textContent)
}
- 防抖
防抖用于减少函数调用次数,对于频繁的调用,只执行这些调用的最后一次。
/**
* @param {function} func - 执行函数
* @param {number} wait - 等待时间
* @param {boolean} immediate - 是否立即执行
* @return {function}
*/
function debounce(func, wait = 300, immediate = false){
let timer, ctx;
let later = (arg) => setTimeout(()=>{
func.apply(ctx, arg)
timer = ctx = null
}, wait)
return function(...arg){
if(!timer){
timer = later(arg)
ctx = this
if(immediate){
func.apply(ctx, arg)
}
}else{
clearTimeout(timer)
timer = later(arg)
}
}
}
- 节流
节流用于减少函数请求次数,与防抖不同,节流是在一段时间执行一次。
/**
* @param {function} func - 执行函数
* @param {number} delay - 延迟时间
* @return {function}
*/
function throttle(func, delay){
let timer = null
return function(...arg){
if(!timer){
timer = setTimeout(()=>{
func.apply(this, arg)
timer = null
}, delay)
}
}
}
- 柯里化
Currying(柯里化)是把接受多个参数的函数变换成接受一个单一参数的函数,
并且返回接受余下的参数而且返回结果的新函数的技术。
通用柯里化函数:
function currying(fn, arr = []) {
let len = fn.length
return (...args) => {
let concatArgs = [...arr, ...args]
if (concatArgs.length < len) {
return currying(fn, concatArgs)
} else {
return fn.call(this, ...concatArgs)
}
}
}
使用:
let sum = (a,b,c,d) => {
console.log(a,b,c,d)
}
let newSum = currying(sum)
newSum(1)(2)(3)(4)
优点:
1)参数复用,由于参数可以分开传入,我们可以复用传入参数后的函数
2)延迟执行,就跟 bind 一样可以接收参数并返回函数的引用,而没有调用
- 垃圾回收
堆分为新生代和老生代,分别由副垃圾回收器和主垃圾回收器来负责垃圾回收。
1.新生代
一般刚使用的对象都会放在新生代,它的空间比较小,只有几十MB,
新生代里还会划分出两个空间:form空间和to空间。
对象会先被分配到form空间中,等到垃圾回收阶段,将form空间的存活对象复制到to空间中,
对未存活对象进行回收,之后调换两个空间,这种算法称之为 “Scanvage”。
新生代的内存回收频率很高、速度也很快,但空间利用率较低,因为让一半的内存空间处于“闲置”状态。
2.老生代
老生代的空间较大,新生代经过多次回收后还存活的对象会被送到老生代。
老生代使用“标记清除”的方式,从根元素开始遍历,将存活对象进行标记。
标记完成后,对未标记的对象进行回收。
经过标记清除之后的内存空间会产生很多不连续的碎片空间,导致一些大对象无法存放进来。
所以在回收完成后,会对这些不连续的碎片空间进行整理。
- JavaScript设计模式
1.单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
JavaScript 作为一门无类的语言,传统的单例模式概念在 JavaScript 中并不适用。
稍微转换下思想:单例模式确保只有一个对象,并提供全局访问。
常见的应用场景就是弹窗组件,使用单例模式封装全局弹窗组件方法:
import Vue from 'vue'
import Index from './index.vue'
let alertInstance = null
let alertConstructor = Vue.extend(Index)
let init = (options)=>{
alertInstance = new alertConstructor()
Object.assign(alertInstance, options)
alertInstance.$mount()
document.body.appendChild(alertInstance.$el)
}
let caller = (options)=>{
// 单例判断
if(!alertInstance){
init(options)
}
return alertInstance.show(()=>alertInstance = null)
}
export default {
install(vue){
vue.prototype.$alert = caller
}
}
无论调用几次,组件也只实例化一次,最终获取的都是同一个实例。
2.策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式是开发中最常用的设计模式,在一些场景下如果存在大量的 if/else,
且每个分支点的功能独立,这时候就可以考虑使用策略模式来优化。
就像就上面手写深拷贝就用到策略模式来实现:
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
const type = Object.prototype.toString.call(obj).slice(8, -1)
// 策略对象
let strategy = {
Date: (obj) => new Date(obj),
RegExp: (obj) => new RegExp(obj),
Array: clone,
Object: clone
}
function clone(obj){
// 防止循环引用,导致栈溢出,相同引用的对象直接返回
if (map.get(obj)) return map.get(obj);
let target = new obj.constructor();
map.set(obj, target);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
target[key] = deepClone(obj[key], map);
}
}
return target;
}
return strategy[type] && strategy[type](obj)
}
这样的代码看起来会更简洁,只需要维护一个策略对象,需要新功能就添加一个策略。
由于策略项是单独封装的方法,也更易于复用。
3.代理模式
定义:为一个对象提供一个代用品,以便控制对它的访问。
当不方便直接访问一个对象或者不满足需要的时候,提供一个代理对象来控制对这个对象的访问,
实际访问的是代理对象,代理对象对请求做出处理后,再转交给本体对象。
使用缓存代理请求数据:
function getList(page) {
return this.$api.getList({
page
}).then(res => {
this.list = res.data
return res
})
}
// 代理getList
let proxyGetList = (function() {
let cache = {}
return async function(page) {
if (cache[page]) {
return cache[page]
}
let res = await getList.call(this, page)
return cache[page] = res.data
}
})()
上面的场景是常见的分页需求,同一页的数据只需要去后台获取一次,并将获取到的数据缓存起来,
下次再请求同一页时,便可以直接使用之前的数据。
4.发布订阅模式
定义:它定义对象间的一种一对多的依赖关系,当一个对象的状态发送改变时,
所有依赖于它的对象都将得到通知。
发布订阅模式主要优点是解决对象间的解耦,它的应用非常广泛,既可以用在异步编程中,
也可以帮助我们完成松耦合的代码编写。像 eventBus 的通信方式就是发布订阅模式。
let event = {
events: [],
on(key, fn){
if(!this.events[key]) {
this.events[key] = []
}
this.events[key].push(fn)
},
emit(key, ...arg){
let fns = this.events[key]
if(!fns || fns.length == 0){
return false
}
fns.forEach(fn => fn.apply(this, arg))
}
}
上面只是发布订阅模式的简单实现,还可以为其添加 off 方法来取消监听事件。
在 Vue 中,通常是实例化一个新的 Vue 实例来做发布订阅中心,
解决组件通信。而在小程序中可以手动实现发布订阅模式,用于解决页面通信的问题。
5.装饰器模式
定义:动态地为某个对象添加一些额外的职责,而不会影响对象本身。
装饰器模式在开发中也是很常用的设计模式,它能够在不影响源代码的情况下,
很方便的扩展属性和方法。比如以下应用场景是提交表单。
methods: {
submit(){
this.$api.submit({
data: this.form
})
},
// 为提交表单添加验证功能
validateForm(){
if(this.form.name == ''){
return
}
this.submit()
}
}
想象一下,如果你刚接手一个项目,而 submit 的逻辑很复杂,可能还会牵扯到很多地方。
冒然的侵入源代码去扩展功能会有风险,这时候装饰器模式就帮上大忙了。。
- 模块化
这里只记录常用的两种模块:CommonJS模块、ES6模块
1.CommonJS模块
Node.js 采用 CommonJS 模块规范,在服务端运行时是同步加载,
在客户端使用需要编译后才可以运行。
特点:
1)模块可以多次加载。但在第一次加载时,结果会被缓存起来,再次加载模块,直接获取缓存的结果
2)模块加载的顺序,按照其在代码中出现的顺序
语法:
1)暴露模块:module.exports = value 或 exports.xxx = value
2)引入模块:require('xxx'),如果是第三方模块,xxx为模块名;
如果是自定义模块,xxx为模块文件路径
3)清楚模块缓存:delete require.cache[moduleName];,缓存保存在 require.cache 中,
可操作该属性进行删除
模块加载机制:
1)加载某个模块,其实是加载该模块的 module.exports 属性
2)exports 是指向 module.exports 的引用
3)module.exports 的初始值为一个空对象,exports 也为空对象,
module.exports 对象不为空的时候 exports 对象就被忽略
4)模块加载的是值的拷贝,一旦输出值,模块内的变化不会影响到值,引用类型除外
module.exports 不为空:
// nums.js
exports.a = 1
module.exports = {
b: 2
}
exports.c = 3
let nums = require('./nums.js') // { b: 2 }
module.exports 为空:
// nums.js
exports.a = 1
exports.c = 3
let nums = require('./nums.js') // { a: 1, c: 3 }
值拷贝的体现:
// nums.js
let obj = {
count: 10
}
let count = 20
function addCount() {
count++
}
function getCount() {
return count
}
function addObjCount() {
obj.count++
}
module.exports = { count, obj, addCount, getCount, addObjCount }
复制代码
let { count, obj, addCount, getCount, addObjCount } = require('./nums.js')
// 原始类型不受影响
console.log(count) // 20
addCount()
console.log(count) // 20
// 如果想获取到变化的值,可以使用函数返回
console.log(getCount()) // 21
// 引用类型会被改变
console.log(obj) // { count: 10 }
addObjCount()
console.log(obj) // { count: 11 }
2.ES6模块
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,
以及输入和输出的变量。
特点:
1)由于静态分析的原因,ES6模块加载只能在代码顶层使用
2)模块不能多次加载同一个变量
语法:
1)暴露模块:export 或 export default
2)引入模块:import
模块加载机制:
1)模块加载的是引用的拷贝,模块内的变化会影响到值
// nums.js
export let count = 20
export function addCount() {
count++
}
export default {
other: 30
}
// 同时引入 export default 和 export 的变量
import other, { count, addCount } from './async.js'
console.log(other) // { other: 30 }
console.log(count) // 20
addCount()
console.log(count) // 21
ES6 模块与 CommonJS 模块的差异:
1)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2)CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。