文章目录
- 1.3.1、如何产生闭包(`closure`)
- 1.3.2、闭包是什么?
- 1.3.3、产生闭包的条件
- 1.3.4、在chrome中的表现
- 1.3.5、常用的闭包
- 1.3.6、闭包的作用
- 1.3.7、闭包的生命周期
- 1.3.8、闭包的应用
涉及知识体系
1、 预备知识
1.1、Object.defineProperty
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
应当直接在
Object
构造器对象上调用此方法,而不是在任意一个Object
类型的实例上调用。
- 用处—> 可以设置一些额外隐藏的属性
- 测试源码
var obj = {} Object.defineProperty(obj, 'a',{ value:3, // 是否可以被写 writable:false, // 是否可以被枚举 enumerable:true }) Object.defineProperty(obj, 'b', { value: 99, writable:true, enumerable:false }) Object.defineProperty(obj, 'c',{ value:1, enumerable:true }) console.log(obj.a, obj.b); // 3 99 obj.a = 100 // 无法更改 obj.b = 666 // 可以更改 console.log(obj.a, obj.b); // 3 666 console.log('enumerable') for(var i in obj){ console.log(i) // a c }
语法
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty()
定义属性时的默认值):
configurable
当且仅当该属性的configurable
键值为true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为false
。enumerable
当且仅当该属性的enumerable
键值为true
时,该属性才会出现在对象的枚举属性中。 默认为false
。
数据描述符还具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为undefined
。writable
当且仅当该属性的writable
键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符
改变。 默认为false
。
存取描述符还具有以下可选键值:
get
属性的 getter 函数,如果没有 getter,则为undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为undefined
。set
属性的 setter 函数,如果没有 setter,则为undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。 默认为undefined
。
注意,getter与setter需要变量中转
var obj = {} var temp = null Object.defineProperty(obj, 'a',{ // 数据劫持 getter get(){ console.log('获取obj.a') return temp }, // setter set(a){ console.log('尝试设置a属性 = ',a) temp = a } }) console.log(obj) obj.a = 99 console.log(obj.a)
技术参考MDN
1.2 函数柯里化
通过函数调用继续返回函数的方式,实现多次接收参数,最后统一处理的函数处理形式
附方便理解的代码
function sum(a){ return(b)=>{ return (c)=>{ return a+b+c } } } console.log(sum(1)(2)(3))
1.3 闭包
1.3.1、如何产生闭包(closure
)
- 当一个函数的内部(子)函数引用了嵌套的外部(父)函数的变量或者函数时,就产生了闭包
1.3.2、闭包是什么?
- 使用chrome调试查看
- 理解一: 闭包就是函数内部的嵌套函数
- 理解二:包含被引用变量或者函数的对象
- 注意: 闭包存在于嵌套的内部函数中
1.3.3、产生闭包的条件
- 函数嵌套
- 内部函数引用了外部函数的数据(变量或者函数)
- 执行外部函数,执行内部函数的定义
1.3.4、在chrome中的表现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LqSYULzJ-1618128834529)(.\pics\image-20210213170315426.png)]
1.3.5、常用的闭包
- 将函数作为另一个函数的返回值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> function fn1(){ var a = 2 function fn2(){ a++ console.log(a) } return fn2 } var f = fn1() // 没有输出 f() // 3 f() // 4 fn1() // 没有输出 fn1()() // 3 </script> </body> </html>
- 将函数作为实参传递给另一个函数调用
function showDelay(msg,time){ setTimeout(function(){ console.log(msg) },time) } showDelay('hello',1000)
1.3.6、闭包的作用
- 使函数内部的变量在函数执行完之后,仍然存活在内存中(延长局部变量的生命周期)
- 让函数外部可以操作函数内部的局部变量
1.3.7、闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(并不是在调用时)
- 死亡:在嵌套内部函数成为垃圾对象时
1.3.8、闭包的应用
1.3.8.1 定义js模块
js模块:
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或者函数
- 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能
- 模块的定义方法1 --> 利用封闭函数封装私有属性,利用全局变量函数暴露接口---->使用的时候:通过全局对象来访问内部方法
(function () { let a = 1; function test() { console.log(++a); } // 浏览器的全局变量 window.$ = function () { return { test:test, }; }; })(); $().test() //2 console.log($().test,'\n-->', $().test()) /** * 3 ƒ test() { console.log(++a); } " -->" undefined */
- 模块的定义方法2 --> 利用闭包定义私有属性,通过return 一个对象,暴露模块API —> 使用的时候需要先new 一个对象, 在通过这个对象来访问私有属性
function myModule(){ // 私有数据 var msg = 'hello XiaoMing' // 私有方法 function big(){ console.log(msg.toUpperCase()) } function small(){ console.log(msg.toLowerCase()) } // 向外暴露对象 return { big, small } }
1.3. 9、闭包的缺点
- 9.1 函数执行完之后,函数内的局部变量没雨释放,占用内存时间会变长
- 9.2 容易造成内存泄漏
- 解决方法
- 能不用闭包就不用
- 及时释放
f=null
1.4、 webpack
- 涉及依赖包
npm -i webpack@4 webpack-cli webpack-dev-server -D
- webpack.config.js配置文件
const path = require('path'); module.exports = { // 模式 开发 mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js' }, devServer: { // 静态文件根目录 contentBase: path.join(__dirname, "www"), // 不压缩 compress: false, port: 8080, // 虚拟打包的路径,bundle.js文件没有真正的生成 publicPath: "/xuni/" } };
ps : 注意在package.json
中新增 "dev": "webpack-dev-server"
,后续通过npm run dev
启动测试工程
⚠以下部分为尚硅谷学习视频补充笔记,可能会看不懂
2、模拟数据响应式
2.1 利用Object.defineProperty
定义隐藏属性
- 使用
getter
以及setter
实现简易数据劫持
样例如1.1, 方便学习,重复贴上
var obj = {} var temp = null Object.defineProperty(obj, 'a',{ // 数据劫持 getter get(){ console.log('获取obj.a') return temp }, // setter set(a){ console.log('尝试设置a属性 = ',a) temp = a } }) console.log(obj) obj.a = 99 console.log(obj.a)
2.2 构建目录文件
2.3 响应式原理回顾
2.4 模拟代码
index.js
import observe from './Observe'; import Watcher from './Watcher'; import Dep from './Dep' var obj = { a:{ m: { n: 5 } }, b:{ c:'hello' }, g: [2,3,44,55,66] } observe(obj) // obj.a = 10 console.log(obj.a.m.n) // 全局位置 new Watcher(obj, 'a.m.n', (val)=>{ console.log('※※※※※', val) }) obj.a.m.n = 88 console.log(obj.a.m.n)
observe函数
import Observer from './Observer' export default function Observe(value){ // 创建observe 函数 // 要求观测的对象必须是Object if(typeof value != 'object'){ // do nothing return ; } // 定义ob var ob = null if(typeof value.__ob !== 'undefined'){ ob = value.__ob }else{ ob = new Observer(value) } return ob; }
Observer类
import def from './utils'; import defineReactive from './defineReactive'; import arrayMethods from './Array'; import observe from './Observe'; import Dep from './Dep'; export default class Observer{ // 将一个正常的对象转换成可观测对象 // 创建类的时候要思考如何实例化 constructor(value){ // 每个Observer的实例成员都有Dep的实例对象 this.dep = new Dep(); // __ob__ 一般为不可枚举 // 这里的this指向的是实例对象而不是类 def(value,'__ob__', this, false); // console.log("我是Observer构造器",value) if(Array.isArray(value)){ // 如果是数组则更改原型链 Object.setPrototypeOf(value,arrayMethods) // 让这个数组变得observe this.observeArray(value) }else{ this.walk(value) } // console.log('构造器结束') } // 遍历 walk(value){ for(let k in value){ defineReactive(value, k) } } // 数组的特殊遍历 observeArray(arr){ for(let i=0; i<arr.length; ++i){ // 逐项观察observe observe(arr[i]) } } }
defineReactive.js
import observe from './Observe'; import Dep from './Dep'; // 构造闭包环境 export default function defineReactive(data, key, val){ // console.log("我是defineReactive",key) // 定义管理依赖成员 const dep = new Dep() if( arguments.length === 2){ // console.log("arguments=",arguments) val = data[key] } // 让子元素进行observe, 至此形成了递归, // 这个递归不是函数自己调用自己,而是多个函数、类循环调用 let childOb = observe(val) Object.defineProperty(data, key,{ // 可枚举 enumerator : true, // 可以被配置 ,比如说可以被delete configurable : true, // 数据劫持 getter get(){ // console.log('获取'+ key + '属性') // 如果现在处于依赖的收集阶段 // 即有watcher被劫持 if(Dep.target){ dep.depend() if(childOb){ childOb.dep.depend() } } return val }, // setter set(newVal){ // console.log(`尝试设置${key}属性 = `,newVal) if(val === newVal){ return } val = newVal // 当设置了新值的时候,这个新值也要被observe childOb = observe(childOb) // 发布订阅模式 // 依赖管理员通知观察者,观察数据是否发生变化, 如果变化了,则保存新数据 dep.notify() } }) }
Array.prototype的七大方法重写
import def from './utils'; // 得到Array.prototype const arrayPrototype = Array.prototype // 以Array.prototype 为原型创建一个arrayMethods对象 const arrayMethods = Object.create(arrayPrototype) const methodsNeedChange = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsNeedChange.forEach(methodName =>{ // 备份原来的方法 // 因为push, pop 等7个函数的功能不能被剥夺 const original = arrayPrototype[methodName] // 把类数组对象转换成数组 const args = [...arguments] // 把数组身上的__ob__取出来, __ob__已经被添加了 // 因为数组肯定不是最高层, // 定义新的方法 def(arrayMethods, methodName, function(){ const ob = this.__ob__ // 有三种方法,push\unshift\shift能够插入新项,现在把插入的新项也要变成observe的 const result = original.apply(this,arguments) let inserted = [] // console.log('arguments',arguments); switch(methodName){ case 'push': case 'unshift': inserted = args; break; case 'splice': // splice(下标,数量, 插入的新项) inserted = args.slice(2); break; } // 判断有没有要插入的项,把新插入的项变成响应式 if(inserted){ ob.observeArray(inserted) } ob.dep.notify(); // console.log('aaaa') return result },false) }) export default arrayMethods;
utils.js
export default function def(obj, key, value, enumerable){ // console.log('开始定义属性',key) Object.defineProperty(obj,key, { value, enumerable, writable:true, configurable:true }) }
重点Dep类
var uid = 0; export default class Dep{ constructor(){ // console.log('我是Dep的构造器'); this.id = uid ++; // 用数组来存储自己的订阅者 subscribes this.subs = []; } // 添加订阅 addSub(sub){ this.subs.push(sub) console.log(this.subs) } // 移除订阅 rmSub(){ // do nothing } // 添加依赖即是Watcher depend(){ // Dep.target 是我们自己指定的一个全局位置 if(Dep.target){ this.addSub(Dep.target) } } // 通知Watcher更新数据 notify(){ // console.log("我是notitfy"); // 浅克隆一份 const subs = this.subs.slice() // 遍历所有对象,看看是否需要更新数据 for(let i=0 ,l=subs.length; i<l; i++ ){ subs[i].update(); } } }
Watcher类
import def from './utils'; import defineReactive from './defineReactive'; import arrayMethods from './Array'; import observe from './Observe'; import Dep from './Dep'; export default class Observer{ // 将一个正常的对象转换成可观测对象 // 创建类的时候要思考如何实例化 constructor(value){ // 每个Observer的实例成员都有Dep的实例对象 this.dep = new Dep(); // __ob__ 一般为不可枚举 // 这里的this指向的是实例对象而不是类 def(value,'__ob__', this, false); // console.log("我是Observer构造器",value) if(Array.isArray(value)){ // 如果是数组则更改原型链 Object.setPrototypeOf(value,arrayMethods) // 让这个数组变得observe this.observeArray(value) }else{ this.walk(value) } // console.log('构造器结束') } // 遍历 walk(value){ for(let k in value){ defineReactive(value, k) } } // 数组的特殊遍历 observeArray(arr){ for(let i=0; i<arr.length; ++i){ // 逐项观察observe observe(arr[i]) } } }
3、Dep类与Watcher类
把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer
的实例成员中都有一个Dep
的实例;
Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件。
依赖就是Watcher。
只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式
当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
代码实现的巧妙之处
Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在
getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中
4、附中介者模式3S入门精髓
- 使用中介者模式之前
----------------------------------The end !---------------------------
对数据响应式的新理解
【详情请看】从模仿开始理解Vue2的数据响应式