前端js进阶—为面试、工作添砖加瓦!

985 阅读19分钟

PS:对前端技术感兴趣的朋友们,可以关注下我的《致力于前端的技术博客》哦!如果对你有帮助,欢迎赠个⭐️,会常更新内容,敬请期待!❤️❤️


js执行机制

浏览器常驻线程

  • js引擎线程(解释执行js、用户输入、网络请求)
  • GUI线程(绘制用户界面、与js主线程是互斥的)
  • http网络请求线程(处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)
  • 定时触发器线程(setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)
  • 浏览器事件处理线程(将click、mouse等交互事件发生后将这些事件放入事件队列中)

大部分浏览器中都是具有这5个线程的,这些线程是通过UI主线程进行协调运作的。

js引擎线程和GUI线程是互斥的: js可以操作DOM元素,进而会影响到GUI的渲染结果,因此js引擎线程与GUI渲染线程是互斥的。也就是说方js引擎线程处于运行状态时,GUI渲染线程将处于冻结状态。

js单线程

JavaScript是基于单线程运行的,同时又是可以异步执行的,一般来说这种既是单线程又是异步的语言都是基于事件来驱动的,恰好浏览器就给JavaScript提供了这么一个环境。

js主线程

上图表达的原理

  • 同步和异步任务分别进入不同的执行”场所“,同步的进入主线程(执行栈),异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue(任务队列)
  • 主线程内的任务执行完毕,会去Event Queue读取对应的函数,进入主线程执行

上述过程会不断重复,也就是常说的Event Loop(事件轮询)。事件轮询与宏任务和微任务密切相关。

定时器

setTimeout的等待时间结束后并不是直接执行的,而是先进入浏览器的一个任务队列。在同步队列结束后再依次调用任务队列中的任务。 setTimeout(function(){},0):如果定义为0ms,是指当js主线程中的执行栈为空时,再将此异步队列中的任务放入主线程执行。但是0ms实际上是达不到的,根据html标准,最低是4ms。况且如果js主线程的执行时间过长,也会依次等待主线程执行完毕,所以此函数的等待时间要超过0ms。 setInterval是每隔一段时间把任务放到Event Queue中,执行机制同setTimeout

宏任务&微任务

在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

宏任务

常见宏任务:I/O、setTimeout、setInterval、setImmediate、requestAnimationFrame

宏任务

微任务

常见微任务:process.nextTick()、MutationObserver、Promise.then/catch/finally

微任务

【任务1】在主线程上添加宏任务与微任务

// 在主线程上添加宏任务与微任务 
// 执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 
console.log('-------start--------');
setTimeout(() => {
    console.log('setTimeout');
    // 将回调代码放入另一个宏任务队列 
}, 0);

new Promise((resolve, reject) => {
    for (let i = 0; i < 5; i++) {
        console.log(i);
    }
    resolve()
}).then(() => {
    console.log('Promise实例成功回调执行')
})

// 将回调代码放入微任务队列 
console.log('-------end--------');

输出结果:

-------start--------
0
1
2
3
4
-------end--------

【任务2】在微任务中创建微任务

// 在微任务中创建微任务 
// 执行顺序:主线程 => 主线程上创建的微任务1 => 微任务1上创建的微任务2 => 主线程上创建的宏任务 
setTimeout(() => console.log(4))
new Promise(resolve => {
    resolve()
    console.log(1)
}).then(() => {
    console.log(3)
    Promise.resolve().then(() => {
        console.log('before timeout')
    }).then(() => {
        Promise.resolve().then(() => {
            console.log('also before timeout')
        })
    })
})
console.log(2)

输出结果:

1
2
3
before timeout
also before timeout
4

【任务3】在宏任务中创建微任务

// 宏任务中创建微任务 // 执行顺序:主线程 => 主线程上的宏任务队列1 => 宏任务队列1中创建的微任务 // 宏任务队列 1 
setTimeout(() => {
    // 宏任务队列 2.1 
    console.log('timer_1');
    setTimeout(() => {
        // 宏任务队列 3 
        console.log('timer_3')
    }, 0)
    new Promise(resolve => {
        resolve()
        console.log('new promise')
    }).then(() => { // 微任务队列 1 
        console.log('promise then')
    })
}, 0)
setTimeout(() => { // 宏任务队列 2.2 
    console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')

输出结果:

========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3

【任务4】在微任务中创建宏任务

// 微任务队列中创建的宏任务 // 执行顺序:主线程 => 主线程上创建的微任务 => 主线程上创建的宏任务 => 微任务中创建的宏任务 // 宏任务1 
new Promise((resolve) => {
    console.log('new Promise(macro task 1)');
    resolve();
}).then(() => {
    // 微任务1 
    console.log('micro task 1');
    setTimeout(() => { // 宏任务3 
        console.log('macro task 3');
    }, 0)
})
setTimeout(() => { // 宏任务2 
    console.log('macro task 2');
}, 1000)
console.log('========== Sync queue(macro task 1) =========='); 

输出结果:

new Promise(macro task 1)
========== Sync queue(macro task 1) ==========
micro task 1
macro task 3
macro task 2

【任务5】综合

console.log('======== main task start ========');
new Promise(resolve => {
    console.log('create micro task 1');
    resolve();
}).then(() => {
    console.log('micro task 1 callback');
    setTimeout(() => {
        console.log('macro task 3 callback');
    }, 0);
})
console.log('create macro task 2');
setTimeout(() => {
    console.log('macro task 2 callback');
    new Promise(resolve => {
        console.log('create micro task 3');
        resolve();
    }).then(() => {
        console.log('micro task 3 callback');
    })
    console.log('create macro task 4');
    setTimeout(() => {
        console.log('macro task 4 callback');
    }, 0);
}, 0);
new Promise(resolve => {
    console.log('create micro task 2');
    resolve();
}).then(() => {
    console.log('micro task 2 callback');
})
console.log('======== main task end ========');

输出结果:

======== main task start ========
create micro task 1
create macro task 2
create micro task 2
======== main task end ========
micro task 1 callback
micro task 2 callback
macro task 2 callback
create micro task 3
create macro task 4
micro task 3 callback
macro task 3 callback
macro task 4 callback

call、apply手写实现与应用

// 手写call 
Function.prototype.myCall = function() {
    let ctx = arguments[0] || window
    ctx.fn = this
    let args = []
    for (let i = 1; i < arguments.length; i++) {
        args.push(`arguments[${i}]`)
    }
    let result = eval(`ctx.fn(${args.join(',')})`)
    delete ctx.fn
    return result
}

// 手写apply 
Function.prototype.myApply = function(ctx, arr) {
    ctx = ctx || window
    ctx.fn = this
    let result = null
    if (!arr) {
        result = ctx.fn()
    } else {
        let args = []
        for (let i = 0; i < arr.length; i++) {
            args.push(`arr[${i}]`)
        }
        result = eval(`ctx.fn(${args.join(',')})`)
    }
    delete ctx.fn
    return result
}

// 开始测试 
let value = 'window'
let obj = {
    value: 'obj'
}

function show(name, age) {
    console.log(this.value);
    return {
        name,
        age
    }
}
console.log(show.myCall(obj, 'ypp', 12));
console.log(show.myApply(obj, ['ypp', 12]));

输出结果:

obj
{ name: 'ypp', age: 12 }
obj
{ name: 'ypp', age: 12 }

函数柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转化为一系列使用一个参数的函数的技术。 前端使用柯里化的用途主要是简化代码结构,提高系统的维护性,一个方法只有一个参数,强制了功能的单一性。很自然就做到了功能内聚,降低了耦合性。

function add() {
    let arr = Array.from(arguments)
    return arr.reduce((pre, cur) => {
        return pre + cur
    }, 0)
}

function FixedCurry(fn) {
    // 这里的arguments是[add, 1, 2] 
    let _args = Array.prototype.slice.call(arguments, 1)
        //相当于[].call(arguments,1) 
    return function() {
        // 这里的arguments是[2, 3] 
        let newArgs = _args.concat([].slice.call(arguments, 0))
            //这里的 newArgs相当于完成了所有参数的拼接 [1,2,2,3] 
        return fn.apply(this, newArgs)
    }
}
let newAdd = FixedCurry(add, 1)
console.log(newAdd(1, 2));
console.log(newAdd(3));
console.log(newAdd(4));

输出结果:

4
4
5
function add() {
    let arr = Array.from(arguments)
    return arr.reduce((pre, cur) => {
        return pre + cur
    }, 0)
}

function FixedCurry(fn) { // 这里的arguments是[add, 1, 2] 
    let _args = Array.prototype.slice.call(arguments, 1) //相当于[].call(arguments,1) 
    return function() { // 这里的arguments是[2, 3] 
        let newArgs = _args.concat([].slice.call(arguments, 0)) //这里的 newArgs相当于完成了所有参数的拼接 [1,2,2,3] 
        return fn.apply(this, newArgs)
    }
}
// 要实现的效果 
// let newAdd = Curry(add) // newAdd(1, 2, 3, 4) // newAdd(1)(2)(3)(4) // newAdd(1, 2)(3)(4) // ... 
function Curry(fn, length) {
    length = length || fn.length //fn.length代表fn函数需要传入的参数个数
    return function() {
        if (arguments.length < length) {
            // combined=[fn,1,2,3,...] 
            let combined = [fn].concat([].slice.call(arguments, 0)) // 递归 
            return Curry(FixedCurry.apply(this, combined), length - arguments.length)
        } else { // 传入的参数个数达到要求时,执行函数 
            return fn.apply(this, arguments)
        }
    }
}
let newAdd = Curry(add, 4)
console.log(newAdd(1, 2, 3, 4));
console.log(newAdd(1)(20)(3)(4));
console.log(newAdd(1, 22)(3)(4));

输出结果:

10
28
30

在ajax请求中应用函数curry化:

function ajax(type, url, data) {
    let xhr = new XMLHttpRequest()
    xhr.open(type, url, true)
    xhr.send(data)
}

const GET = 'get'
const POST = 'post'
ajax(POST, 'www.test1.com', "age=12")
ajax(POST, 'www.test2.com', "age=20")
ajax(POST, 'www.test23com', "age=23")

let ajaxCurry = curry(ajax)

let post = ajaxCurry(POST)
post('www.test1.com', 'age=12')

let postFromTest = post('www.test1.com')
postFromTest('age=12')

数据扁平化

数据扁平化是指将多维的数组压成一维的数组,不存在数组嵌套的问题。

// 1、方法一 
// 初始版本 
function flatten(arr = []) {
    let resArr = []
    let len = arr.length
    for (let i = 0; i < len; i++) {
        if (Array.isArray(arr[i])) {
            resArr = resArr.concat(flatten(arr[i]))
                //递归 
        } else {
            resArr.push(arr[i])
        }
    }
    return resArr
}

// 挂载原型链上 
Array.prototype.flatten = function() {
    let resArr = []
    this.forEach(item => {
        Array.isArray(item) ? resArr = resArr.concat(item.flatten()) : resArr.push(item)
    })
    return resArr
}

// 2、方法二 
// reduce实现(精简) 
function flatten(arr = []) {
    return arr.reduce((prev, cur) => {
        return Array.isArray(cur) ? prev.concat(flatten(cur)) : prev.concat(cur)
    }, [])
}
// 简化写法 
const flatten = arr => arr.reduce((prev, cur) => {
    return Array.isArray(cur) ? prev.concat(flatten(cur)) : prev.concat(cur)
}, [])

惰性函数

惰性函数是指在函数调用后,改变函数的实现,这样就不需要在每次调用此函数时都去根据条件选择具体的实现,有几个典型的应用场景:

  • 用户vip会员判断(一次判断后固定优惠策略)
  • 浏览器内核检测
  • 事件绑定函数封装
  • ……

示例代码如下:

// 场景1:永远打印第一次调用时的时间 
let printTime = (function() {
    let t = null
    return function() {
        if (t)
            return t
        t = new Date().getTime()
        return t
    }
})()

// 以上版本有个问题,下次再调用时依次会走判断逻辑 
let printTime = function() {
    let t = new Date().getTime()
    test = function() {
        return t
    }
    return test
}

// 场景2:事件绑定方法邓庄 
function addEvent(dom, type, handler) {
    if (dom.addEventListener) {
        dom.addEventListener(type, handler, false)
        addEvent = function(dom, type, handler) {
            dom.addEventListener(type, handler, false)
        }
    } else {
        dom.attachEvent('on' + type, handler)
        addEvent = function() {
            dom.attachEvent('on' + type, handler)
        }
    }
}

函数组合

函数组合是指将多个函数的实现,糅合到一个函数(功能组合),示例如下:

// 古老版本,但是如果传入的是很多个函数呢? 
function comose(f, g) {
    return function(x) {
        return f(g(x))
    }
}
// 依次传入很多个函数f1,f2,f3,f4...,要返回一个组合函数f1(f2,f3,f4(x)) 
function compose() {
    let args = Array.prototype.slice.call(arguments) //变为数组 
    let idx = args.length - 1
    return function(x) {
        let result = args[idx](x)
        while (idx--) {
            result = args[idx](result)
        }
        return result
    }
}
// 进一步优化 
function compose() {
    let args = Array.prototype.slice.call(arguments) //变为数组 
    return function(x) {
        return args.reduceRight((prev, cur) => {
            return cur(prev)
        }, x)
    }
}
// 进一步简化 
const compose = (...args) => x => args.reduceRight((prev, cur) => {
    return cur(prev)
}, x)

// 测试
const f = compose(fn1, fn2, fn3)
console.log(f('dferfes'));

函数记忆

函数记忆是针对需要重复计算和获取值的场景下设置的,类似于动态规划的思想,将每一个状态的前一个状态记录下来,这样可以加速运算~

let count = 0,
    cache = []

function fn(n) {
    if (cache[n]) {
        return cache[n]
    } else { // 
        if (n === 0 || n === 1) {
            cache[0] = 1
            cache[1] = 1
            return 1
        } // 
        cache[n] = n * fn(n - 1)
        return cache[n]
    }
}
// 优化,缓存应该是私有的 
function memorize(fn) {
    let cache = {} //对象的查找速度比数组更快
    return function() { // 保证key唯一 
        let key = arguments.length + Array.prototype.join.call(arguments)
        if (cache[key]) {
            return cache[key]
        } else {
            cache[key] = fn.apply(arguments)
            return cache[key]
        }
    }
}

防抖和节流

防抖

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
// 防抖:非立即执行版
function debounce1(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
} // 防抖:立即执行版 
function debounce2(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);
        let callNow = !timeout;
        timeout = setTimeout(() => {
            timeout = null;
        }, wait)
        if (callNow) func.apply(context, args)
    }
}
// 防抖:双剑合璧版 
/**
 * 
 * @desc 函数防抖 
 * @param func 函数 
 * @param wait 延迟执行毫秒数
 * @param immediate 立即执行 true 表立即执行,false 表非立即执行 
 */
function debounce3(func, wait, immediate) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        } else {
            timeout = setTimeout(function() {
                func.apply(context, args)
            }, wait);
        }
    }
}

节流

节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断 页面频繁点击
// 节流:时间戳版 
function throttle(func, wait) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
// 节流:定时器版 
function throttle(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
            }, wait)
        }
    }
}
// 节流:双剑合璧版 
/** 
 * @desc 函数节流 
 * @param func 函数 
 * @param wait 延迟执行毫秒数 
 * @param type 1 表时间戳版,2 表定时器版 
 */
function throttle(func, wait, type) {
    if (type === 1) {
        let previous = 0;
    } else if (type === 2) {
        let timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if (type === 1) {
            let now = Date.now();
            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        } else if (type === 2) {
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

剖析new运算符

new:创建对象类型的实例

分为以下几步:

  • 创建一个空对象
  • 设置原型链
  • 将空对象作为 this,调用构造函数并传入参数
  • 如果构造函数返回值为对象或者函数,则返回该对象或者函数,否则返回创建的新对象
// 1. 访问Obj构造函数里面的属性
// 2. 访问到Obj.prototype里面的方法

function MyNew() {
  let o = {}
  // arguments对象上没有shift方法,可以调用Object对象上的shift方法
  let constr = Object.prototype.shift.call(arguments) // 得到对象的构造函数
  o.__proto__ = constr.prototype // 把对象原型赋值给o的原型链
  constr.apply(o, arguments) // 让o拥有constr里面的属性

  return o
}

使用以上定义的方法,创建出一个对象:

var p=MyNew(Student,'Zhang',12)

考虑构造函数直接返回对象的情况:

function MyNew() {
  let o = {}
  // arguments对象上没有shift方法,可以调用Object对象上的shift方法
  let constr = Object.prototype.shift.call(arguments) // 得到对象的构造函数
  o.__proto__ = constr.prototype // 把对象原型赋值给o的原型链
  let result = constr.apply(o, arguments) // 让o拥有constr里面的属性

  return typeof result === 'object' ? result || o : o
}

**思考:**此时还存在问题吗?

还存在问题。如var obj={}等同于var obj=new Object(),我们要实现new,但是代码中用到了new,如果本来就没有new呢?这就陷入了“先有鸡还是先有蛋”的问题。应该使用Object.create(null)——创建一个没有原型的对象。修改如下:

function MyNew() {
  let o = Object.create(null)
  // arguments对象上没有shift方法,可以调用Object对象上的shift方法
  let constr = Object.prototype.shift.call(arguments) // 得到对象的构造函数
  o.__proto__ = constr.prototype // 把对象原型赋值给o的原型链
  let result = constr.apply(o, arguments) // 让o拥有constr里面的属性

  return typeof result === 'object' ? result || o : o
}

但是又出现了问题:发现原型上的属性不见了。因为我们Object.create(null)将原型设置为了null,后面就无法给原型链赋值。重新修改如下:

function MyNew() {
  let constr = Object.prototype.shift.call(arguments)
  let o = Object.create(constr.prototype)
  let result = constr.apply(o, arguments)
  return typeof result === 'object' ? result || o : o
}

闭包原理及应用场景

闭包的原理:

  • 作用域链
  • V8引擎解析js原理——语法检查阶段、运行阶段(预解析阶段、执行阶段)

这里的Scopes是什么? undefined Scopes是作用域属性,保存自身作用域变量对象和父级变量对象引用,因此函数得以访问到上级的函数中变量,这样才形成了一个作用域链。

闭包应用场景: 防抖:

function debounce(fn, wait) {
  let timeout = null
  return args => {
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(fn, wait)
  }
}

如果我们在定义构造函数原型的时候,写如下的形式,会带来什么问题吗?

function Obj(name, age) {
  this.name = name.toString()
  this.age = age.toString()
}
Obj.prototype = {
  getName: function () {
    return this.name
  },
  getAge: function () {
    return this.age
  }
}

js一般不建议以上的写法,因为o.prototype={}会修改整个原型,可以按照以下方法书写:

function Obj(name, age) {
  this.name = name.toString()
  this.age = age.toString()
}
Obj.prototype.getName = function () {
  return this.name
}
Obj.prototype.getAge = function () {
  return this.age
}

V8引擎运行机制原理(掌握GO/AO)

web应用开发时是需要关注内存问题的:

  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应
  • Node使用的也是v8,内存对于后端服务的性能至关重要,因为服务的持久性,后端更容易造成内存泄漏

v8引擎内存回收机制

v8内存分配:

undefined

  • 内存空间和操作系统有关,64位为1.4G,32位为0.7G
  • 64位下新生代的空间为64MB,老生代为1400MB
  • 32位下新生代的空间为16MB,老生代为700MB

了解: 为什么v8引擎分配的内存这么少?

  • js作为页面的运行脚本,与服务端语言最大的区别是运行完关闭,不会持续地占用内存,整个内存空间全部清空,1.4G对于脚本程序来说绰绰有余了(表层原因)
  • 在v8引擎垃圾回收时,会暂停掉整个程序的执行,得等到垃圾回收完成了再继续执行后面的程序,所以内存不能设置太大,否则垃圾回收时间过长(深层原因)

新生代内存空间存放的是一些存活时间较短的变量,老生代内存空间存放程序运行中始终未被销毁的变量

垃圾回收算法

1)新生代垃圾回收算法 简单地说就算复制(牺牲空间换取时间)

分别两片空间(from、to),将所有变量从from复制到to,挑出所有程序还需要使用到的变量重新放到from,剩下的不需要的变量在to中清空。以此循环。打个比方,from中存放了全部苹果,我们将from中的苹果倒入to,挑选出好的苹果放入from,清理掉所有坏的苹果。之所以不在from中直接清理无用变量,是因为这样要一个个标记无用变量,而且在内存中会产生磁盘碎片。

2)老生代垃圾回收算法 就是标记删除整理

标记死掉的变量,会产生磁盘碎片,然后再整理蒜片(牺牲时间换空间,删除一个变量,后面往挪一格)

undefined

什么样的变量能从新生代到老生代?

  • 判断是否复制过,而且变量还活着
  • to空间是否使用了25% 满足以上两个条件后,会将变量从新生代放入老生代。

undefined

v8是如何处理变量的?

  • 内存是存储变量等数据的
  • 局部变量当程序运行结束,且没有引用的时候就会随之消失
  • 全局对象会始终存活到程序运行结束

我们可以采用node提供的process.memoryUsage()函数来查看内存的使用情况:

> process.memoryUsage()
{ rss: 21438464,
  heapTotal: 7684096,
  heapUsed: 4928152,
  external: 8907 }

heapTotal和heapUsed是V8堆内存的使用情况,heapTotal是已申请到的堆内存,heapUsed是当前使用的量。

内存优化技巧:

  • 尽量不要定义全局变量
  • 全局变量记得销毁掉(在定义全局变量后,如果我们不需要在使用到它们,要记得释放——设置为null)
  • 使用匿名自执行函数变全局为局部(减少全局变量)
  • 尽量避免闭包

防止内存泄漏:

  • 滥用缓存(可以限制缓存数组长度)
  • 大内存量操作(node读取大文件,变为流式读取)

v8引擎内存优化

前端应该了解一下v8引擎内存回收机制,掌握内存优化技巧

html文档加载顺序

  1. 创建内存结构 空间:堆>池>栈,其中栈负责存放变量值的空间地址,池存放变量的值,堆存放对象的值。我们通过操作变量就可以操作内存空间,这就是内存的原理。

undefined

  1. V8读取文档(从上至下、从左至右)
  2. V8初始化BOM和DOM树对象

undefined

  1. V8创建GO对象(预加载或者提升)
  2. 执行script脚本
  3. 事件执行script脚本(鼠标点击等等)

GO 全局作用域window对象 / AO 局部作用域对象

闭包和原型链 constructor、prototype、__proto__区别:

undefined

GO & AO

js执行前会进行预编译

全局预编译GO(Global Object)

  • 创建GO对象
  • 给全局变量赋值 undefined GO{a: undefined}
  • 将全局的函数申明的函数名作为key,value为函数整体赋值到GO对象中 函数预编译AO (Activation Object)
  • 创建AO对象
  • 将函数内的形参和变量声明存储到AO对象中,值为undefined
  • 将实参和形参统一
  • 将函数内的函数申明的名称作为AO对象的key,函数的整体内容为value 存储到AO对象

看一下以下代码,运行后分别执行了哪些操作,最后输出结果是什么?

function test(a, b) {
    console.log(a); //function a
    var c = 123
    console.log(c); //123

    function a() {}
    console.log(b); //3
    var b = function c() {}
    console.log(b); //function c
}
test(1, 3)

是不是感觉有点复杂?我们先分析一下分别执行了哪些操作?

  • 创建一个AO对象{}
  • 将函数内所有的形参和变量声明储存到AO对象中,value为undefined
  • 将形参和实参进行统一
  • 将所有函数声明的函数名作为AO对象的key,函数整体内容为value储存到AO对象中

最终的输出结果如下:

output:
[Function: a]
123
3
[Function: c]

实现发布-订阅模式

首先,实现一个最简单版本:

// 初始版本
let obj = {}
obj.list = []
obj.listen = function(fn) {
    this.list.push(fn)
}
obj.trigger = function() {
    for (let i = 0, fn; fn = this.list[i++];) {
        fn.apply(this, arguments)
    }
}

// --------------------------------------------
// 开始测试
obj.listen(function(msg) {
    console.log('person1:', msg);
})
obj.listen(function(msg) {
    console.log('person2:', msg);
})
obj.trigger('message')

还存在问题—— 每次发布消息时,所有订阅者都收到了,显然会对订阅者产生困扰,应该根据消息类型对相应订阅者发布即可

let obj = {}
obj.list = {} //不建议是数组,建议为对象
obj.listen = function(key, fn) {
    // if (!this.list[key]) {
    //     this.list[key] = []
    // }
    // this.list[key].push(fn)

    // 简写形式
    (this.list[key] || (this.list[key] = [])).push(fn)
}
obj.trigger = function() {
    // 取出消息类型名称(第一个参数)
    const key = Array.prototype.shift.call(arguments)
    const fns = this.list[key]
    if (!fns || fns.length === 0) return
    for (let i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments)
    }
}


// --------------------------------------------
// 开始测试
obj.listen('big', function(msg) {
    console.log('person1:', msg);
})
obj.listen('small', function(msg) {
    console.log('person2:', msg);
})
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

还存在问题—— 代码未封装,应该放到单独对象中,做到高维护性、低耦合性

// 封装
let event = {
    list: {},
    listen: function(key, fn) {
        (this.list[key] || (this.list[key] = [])).push(fn)
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments)
        const fns = this.list[key]
        if (!fns || fns.length === 0) return
        for (let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    }
}

// 为对象添加发布订阅功能
let initEvent = function(obj) {
    for (const i in event) {
        obj[i] = event[i]
    }
}


// --------------------------------------------
// 开始测试
let obj = {}
initEvent(obj)

obj.listen('big', function(msg) {
    console.log('person1:', msg);
})
obj.listen('small', function(msg) {
    console.log('person2:', msg);
})
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

还存在问题—— 未实现删除订阅的功能

let event = {
    list: {},
    listen: function(key, fn) {
        (this.list[key] || (this.list[key] = [])).push(fn)
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments)
        const fns = this.list[key]
        if (!fns || fns.length === 0) return
        for (let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    },
    remove: function(key, fn) {
        const fns = this.list[key]
        if (!fns) return false
        if (!fn) {
            fn && (fns.length = 0)
        } else {
            for (let i = 0; i < fns.length; i++) {
                let _fn = fns[i]
                _fn === fn && (fns.splice(i, 1))
            }
        }
    }
}

// 为对象添加发布订阅功能
let initEvent = function(obj) {
    for (const i in event) {
        obj[i] = event[i]
    }
}


// --------------------------------------------
// 开始测试

let obj = {}
initEvent(obj)

obj.listen('big', fn1 = function(msg) {
    console.log('person1:', msg);
})
obj.listen('small', fn2 = function(msg) {
    console.log('person2:', msg);
})

console.log('删除前:');
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

obj.remove('big', fn1)
obj.remove('small', fn2)

console.log('删除后:');
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

还存在问题——

  • 给每个对象都添加了list缓存列表、listen/trigger/remove方法,这其实是一种资源的浪费(占内存),因为不一定可以使用到
  • 订阅者和event还是存在耦合性,订阅者必须要知道event对象的名字以及订阅事件名称,才可以实现订阅
let Event = (function() {
    let list = {},
        listen, trigger, remove

    listen = function(key, fn) {
            (list[key] || (list[key] = [])).push(fn)
        },
        trigger = function() {
            const key = Array.prototype.shift.call(arguments)
            const fns = list[key]
            if (!fns || fns.length === 0) return
            for (let i = 0, fn; fn = fns[i++];) {
                fn.apply(this, arguments)
            }
        }

    remove = function(key, fn) {
        const fns = list[key]
        if (!fns) return false
        if (!fn) {
            fn && (fns.length = 0)
        } else {
            for (let i = 0; i < fns.length; i++) {
                let _fn = fns[i]
                _fn === fn && (fns.splice(i, 1))
            }
        }
    }

    return {
        listen,
        trigger,
        remove
    }
})()

// 为对象添加发布订阅功能
let initEvent = function(obj) {
    for (const i in Event) {
        obj[i] = Event[i]
    }
}


// --------------------------------------------
// 开始测试

let obj = {}
initEvent(obj)

obj.listen('big', fn1 = function(msg) {
    console.log('person1:', msg);
})
obj.listen('small', fn2 = function(msg) {
    console.log('person2:', msg);
})

console.log('删除前:');
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

obj.remove('big', fn1)
obj.remove('small', fn2)

console.log('删除后:');
obj.trigger('big', 'message1')
obj.trigger('small', 'message2')

这样逐渐实现了一个较为完备的发布-订阅模式。