串联二:
老生常谈,请你谈一下闭包?
假若你认为此题简单,一两句话就能说完?那当真浮于表面。此题实则一两天都说不完!它可以牵扯出 js 原理的大部分知识。是真正意义上的“母题”。
一图胜万言
- 原创脑图,转载请说明出处
串联知识点:闭包、作用域、原型链、js继承。
串联记忆:此题并非像上文题“从输入URL到页面加载发生了什么?”,后者“串联点”是按解答步骤来递进的。而这里的“串联点”,更多是你中有我,我中有你,前后互相补充,互相完善。当你领略完的时候,一定会有一种“万物归宗”的感觉。
归并为一五言诗来记忆:
闭包作用域
原型多考虑
继承八大法
基础好好叙
一、闭包
闭包含义
一言以蔽之。
在一个函数内有另外一个函数可以访问它的内部变量,并且另外一个函数在外部被调用,这样的词法环境叫闭包。
- 作用:
- 读取函数内部的变量;(私有变量、不污染全局)
- 让变量始终保存在内存中。
闭包应用
- 最经典试题
for(var i = 0; i < 5; i++){ (function(j){ setTimeout(function(){ console.log(j); },1000); })(i); } console.log(i);
垃圾回收机制
- js 垃圾回收机制:标记清除和引用计数。
标记清除简单讲就是变量存储在内存中,当变量进入执行环境的时候,垃圾回收器会给它加上标记,这个变量离开执行环境,将其标记为“清除”,不可追踪,不被其他对象引用,或者是两个对象互相引用,不被第三个对象引用,然后由垃圾回收器收回,释放内存空间。
防抖、节流函数
- 防抖
function debounce(fn, delay) { var timer; // 维护一个 timer return function () { var _this = this; // 取debounce执行作用域的this var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args); }, delay); }; }
- 节流
function throttle(fn, delay) { var timer; return function () { var _this = this; var args = arguments; if (timer) { return; } timer = setTimeout(function () { fn.apply(_this, args); timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器 }, delay) } }
二、作用域
全局作用域
- 直接编写在script标签中的JS代码,都在全局作用域;
- 全局作用域在页面打开时创建,在页面关闭时销毁;
- 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用;
- 全局作用域中,创建变量都会作为window对象的属性保存;
- 创建的函数都会作为window对象的方法保存;
- 全局作用域中的变量都是全局变量,在页面的任何部分都可以访问的到;
我们可以在控制台直接打印试试看,正如以上所说:
函数作用域(局部作用域)
- 变量在函数内声明,变量属于局部作用域。
- 局部变量:只能在函数内部访问。
- 局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
- 局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。
块级作用域
块级作用域 : 块级作用域指的就是使用 if () { }; while ( ) { } ......这些语句所形成的语句块 , 并且其中变量必须使用 let 或 const 声明,保证了外部不可以访问语句块中的变量。
注:函数作用域和块级作用域没有直接关系。
- const、let、var 区别
- const 声明则不能改变,块级作用域,不允许变量提升。
- let 块级作用域,不允许变量提升。
- var 非块级作用域,允许变量提升。
作用域链
出现函数嵌套函数,则就会出现作用域链 scope chain。
- 遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
- 局部作用域(如函数作用域)可以访问到全局作用域中的变量和方法,而全局作用域不能访问局部作用域的变量和方法。
用作用域链来解释闭包:
function outter() { var private= "I am private"; function show() { console.log(private); } // [[scope]]已经确定:[outter上下文的变量对象,全局上下文变量对象] return show; } var ref = outter(); console.log(private); // outter执行完以后,private不会被销毁,并且只能被show方法所访问, //直接访问它会出现报错:private is not defined ref(); // 打印I am private 复制代码
其实,我们要明白的是函数的声明和调用是分开的,如果不搞清楚这一点,很多基础面试题就容易出错。
变量生命周期
一个变量的声明意味着就是我们在内存当中申请了一个空间用来存储。这个内存也就是我们电脑的运行内存,如果我们一直的声明变量,不释放的话。会占用很大的内存。
在 c/c++ 当中是需要程序员在合适的地方手动的去释放变量内存,而 javascript 和 java 拥有垃圾回收机制(咱们在上文已说明)。
js 变量分为两种类型:全局变量和局部变量
- 全局变量的生命周期:从程序开始执行创建,到整个页面关闭时,变量收回。
- 局部变量的生命周期:从函数开始调用开始,一直到函数调用结束。 但有的时候我们需要让局部变量的生命周期长一点,此时就用到了闭包。
三、原型链
实例与原型
一个原型对象的隐形属性指向构造它的构造函数的显示属性。
当一个对象去查找它的属性,找不到就去找他的构造函数的属性,一直向上找,直到找到 Object()。
判断数据类型
- typeof
- instanceof
- constructor
- Object.prototype.toString.call()
new 一个对象
步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
this 指向
this 指向 5 大规则:
- 如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的函数。
- 如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数。
- 如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象
- 如果一个函数作为FFI被调用,意味着这个函数不符合以上任意一种调用方式,this指向全局对象,在浏览器中,即是window。
- 如果出现上面对条规则的累加情况,则优先级自1至4递减,this的指向按照优先级最高的规则判断。
参考:this指向记忆5大原则
- 箭头函数中的 this 指向:箭头函数中的this是在定义函数的时候绑定,而不是在执行函数的时候绑定。
bind、call、apply
- call call()方法接收的第一个参数和apply()方法接收的一样,变化的是其余的参数直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来。
function sum(num1 , num2){ return num1 + num2; } function callSum(num1 , num2){ return sum.call(this , sum1 , sum2); } console.log(callSum(10 , 10)); // 20 复制代码
- apply apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组,这里的参数数组可以是Array的实例,也可以是arguments对象(类数组对象)。
function sum(num1 , num2){ return num1 + num2; } function callSum1(num1,num2){ return sum.apply(this,arguments); // 传入arguments类数组对象 } function callSum2(num1,num2){ return sum.apply(this,[num1 , num2]); // 传入数组 } console.log(callSum1(10 , 10)); // 20 console.log(callSum2(10 , 10)); // 20
call和apply的区别在于二者传参的时候,前者是一个一个的传,后者是传数组或类数组arguments
- bind
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
手写深浅拷贝
浅:
function clone(target) { let cloneTarget = {}; for (const key in target) { cloneTarget[key] = target[key]; } return cloneTarget; }; 复制代码
深(递归):
function clone(target) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; for (const key in target) { cloneTarget[key] = clone(target[key]); } return cloneTarget; } else { return target; } };
了解更多,推荐阅读:如何写出一个惊艳面试官的深拷贝?
四、js 继承
八种继承方式,详细请看此篇:JavaScript常用八种继承方案。
本瓜不做赘述,可列二三关键必记。
串联三:
请你谈谈 Vue 原理?
本瓜不装了,摊牌了。其实本文的目录结构编写时间线在 《 Vue(v2.6.11)万行源码生啃,就硬刚!》这篇文章之前。当时就是因为似懂非懂,才定下心来“生啃源码”。现在源码看完了,体会的确又不一样了。但由于细节太多,篇幅受限。此处也仅列框架、点出要点、注释链接,以便记忆。
一图胜万言
- 原创脑图,转载请说明出处
串联知识点:Vue初始化和生命周期、虚拟DOM、响应式原理、组件编译、Vue常用补充、Vue全家桶。
串联记忆:编一顺口溜,见笑。
V U E 真容易
初始化 有生命
虚拟 dom 好给力
响应式 看仔细
组件化 大家利
全家桶 笑嘻嘻
会打包 挣一亿
- 邀大家来改编
一、init&render
挂载和初始化
new Vue()发生了什么?
Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的。Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法。
初始化主要实现:合并配置(mergeOptions),初始化生命周期(initLifecycle),初始化事件中心(initEvents),初始化渲染(initRender),初始化 data、props、computed、watcher 等等。
流程图参考如下:
- 此图在组件编译环节少了 optimize ,可能由于版本差异。 Vue2.4.4 源码
- 借图,未找到真实出处,保留引用说明坑位。
实例生命周期
生命周期图示,还是得看官网文档。还记得这句话吗?
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
注释版:
推荐:源码解读
要点注释:
- beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的。从源码中可以看到 beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等属性。那么显然 beforeCreate 的钩子函数中就不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。而 created 钩子函数可以。
Vue.prototype._init = function (options?: Object) { // ... initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') // beforeCreate 钩子 initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') // created 钩子 // ... }
- 在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mounted 钩子。(此点重要)
- beforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中调用。update 的执行时机是在 flushSchedulerQueue 函数调用的时候。
- beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段。
- activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子。
重点说明:
- 在 Vue2 中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,用来把实例渲染成一个虚拟 Node(Virtual DOM)。
二、虚拟DOM
Vue 2.0 相比 Vue 1.0 最大的升级就是利用了 Virtual DOM。
vdom
vdom 其实就是一颗 js 对象树,最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。原本对 DOM 节点的操作(浏览器将 DOM 设计的非常复杂)转成了对 js 对象的操作,加快处理速度、提升性能。
VNode 的创建是由 createElement 方法实现的。
欲知原理,推荐阅读:snabbdom
diff & patch
在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。即用 diff 算法比较差异,然后调用 patch 应用到真实 DOM 上去。patch 的过程即一个打补丁的过程。
diff 算法是一个交叉对比的过程,大致可以简要概括为:头头比较、尾尾比较、头尾比较、尾头比较。
入门级别 diff 详情推荐看此篇:LINK
注意:render函数返回的是 vdom,patch生成的才是真实DOM。
三、响应式原理
官方生图,高屋建瓴。
本瓜曾在《简析 vue 的双向绑定原理》这篇文章写过,如今看又是一番心情。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
发布订阅者模式(字节考题)
class emit { } cosnt eeee = new emit() eeee.on('aa' , function() { console.log(1)}) eeee.on('aa' , function() {console.log(2)}) eeee.emit('aa') //class emit{}
// 要求手写发布者-订阅模式
class Subject{ constructor () { this.observers =[] } add (observer) { this.observers.push(observer) } notify () { this.observers.map((item, index) => { item.update() }) } } class Observer { constructor (name) { this.name = name } update () { console.log("I`m " + this.name) } } var sub = new Subject() var obs1 = new Observer("obs1") var obs2 = new Observer("obs2") sub.add(obs1) sub.add(obs2) sub.notify() // I`m obs1 I`m obs2
除了“发布者订阅模式”,你还知道哪些 js 设计模式?这里留个坑,以后再补,东西太多了......
Observe
Observe 的功能就是用来监测数据的变化。它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:
这里贴一下源码片段,咱可以感受下:
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) ... } walk (obj: Object) { ... } observeArray (items: Array<any>) { ... } }
有没有觉得和上面提到的“发布订阅者模式”中的相似。Observer 首先实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value 的 ob 属性上。(def 函数是一个简单的对 Object.defineProperty 的封装)。
Dep
Dep 是整个 getter 依赖收集的核心。
由于 Watcher 是有多个的,所以需要用 Dep 收集变化之后集中管理,再通知到对应的 Watcher。由此也好理解 Dep 是依赖于 Watcher 的。
贴源码片段,感受一下:
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first ... }
Watcher
贴源码片段,感受一二:
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; computed: boolean; sync: boolean; dirty: boolean; active: boolean; dep: Dep; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options ... // parse expression for getter ... } get () { pushTarget(this) ... } addDep (dep: Dep) { const id = dep.id ... } cleanupDeps () { ... } // ... }
Watcher 会通知视图的更新 re-render。
常见视图更新场景:
- 数据变 → 使用数据的视图变(对应:负责敦促视图更新的render-watcher)
- 数据变 → 使用数据的计算属性变 → 使用计算属性的视图变(对应:执行敦促计算属性更新的computed-watcher)
- 数据变 → 开发者主动注册的watch回调函数执行(对应:用户注册的普通watcher(watch-api或watch属性))