IIFE(立即执行函数)
在 JavaScript 中,IIFE 被广泛应用。
比如,我们经常能在一些 JavaScript 项目中,看到所有代码被包裹在一个 IIFE 中
(function(){ // 项目实现代码})();IIFE 的使用理由
使用 IIFE 的一个重要原因是,可以避免对外层作用域的污染。
因为IIFE的本质就是一个函数,我们在函数内部定义的所有变量、函数,均在函数本地生效,避免了跟其他 JavaScript 冲突。
声明变量务必使用 var(ES6 之后支持 let,const)。 因为非 strict mode 中,不使用 var 声明的变量,实际上是定义在全局作用域的。
此外还有一些有用的特性也是使用 IIFE 的理由之一,例如提供正确的 undefined 值:
(function(undefined){ // 即使 undefined 被重新赋值,通过故意不传实参,也可确保形参 `undefined` 是真正的 `undefined`})();当然利用这种模式,也可以往 IIFE 里面注入一些其他的依赖,比如全局对象等等。
IIFE 的执行原理
奥妙就在于 () 上面,因为()运算符内部必须是一个表达式。
所以,() 内部的函数不是一个函数声明,而是一个函数表达式,表达式的计算结果返回一个函数,然后立即被调用。
这跟下面的函数表达式一样道理(= 运算符右侧接受一个表达式):
var exprVal = function() { return 'haha~'}()// exprVal === 'haha~'IIFE 的其他写法
既然明白了 IIFE 的原理就是利用的函数表达式的性质,那么我们就可以有很多种写法了,例如
(function(){ /* 代码 */ })();(function(){ /* 代码 */ }());!function(){ /* 代码 */ }();+function(){ /* 代码 */ }();void function(){ /* 代码 */ }();选择哪种写法基本取决于个人审美,不过惯例上使用括号的比较多。
但,不同的写法仍有一些其他的细微影响。
比如分号的使用,比如括号的方式,因为括号的多义性,如果前面还有内容,那么括号会被当作是一个函数调用,导致非期望的结果出现。
一个好的实践方式是,在括号、中括号等前面随手加上分号:
;(function(){ /* 代码 */ })()这样可以避免 IIFE 前面的代码忘记加分号导致的问题。
READ MORE...DOM 节点集合及其活动(live)、静态(static)之分
我们批量处理页面上元素的时候,需要先选择出对应的节点集合,常用的有以下 API
document.getElementsByClassName()- 返回一个 HTMLCollection,且是活动的
document.getElementsByTagName()- 返回一个 HTMLCollection,且活动的
document.getElementsByName()- 返回一个 NodeList,且是活动的
document.querySelectorAll()- 返回一个 NodeList,且是静态的
上面出现了两种 collection,一种是 HTMLCollection ,一种是 NodeList。 那么这两种集合有什么区别呢?
NodeList
一个 NodeList 是一组节点(nodes)的集合。
HTMLCollection
一个 HTMLCollection 是一组元素(elements)的集合。 有个 namedItem 方法是 NodeList 中没有的。
HTMLCollection 是一个历史产物,标准设计新 API 不会再使用这个接口了。
说完两种集合,再来解释 live、static 的概念
活动的(live)、静态的(static)
一个活动的集合,意味着在文档改变的时候,会自动更新这个集合。 而一个静态的集合,则只是生成了一个选择时的快照( snapshot ),不会自动刷新。
看代码:
HTML:
<!-- 省略前面 --><body> <p></p></body><!-- 省略后面 -->JavaScript:
var static = document.querySelectorAll('p')var live = document.getElementsByTagName('p')
// 集合初始都包含一个`p`console.log(static.length) // 1console.log(live.length) // 1
// 往`body`里插入一个 `p`document.body.appendChild(document.createElement('p'))
// 活动的集合会自动刷新,静态的集合不变console.log(static.length) // 1console.log(live.length) // 2live 特性可能带来的问题
因为文档的改变会反应到活动的集合上,所以我们在循环处理一个活动的集合的时候,必须非常小心,不要在循环体上插入符合选择条件的元素,否则可能出现死循环。
例子代码:
var live = document.getElementsByTagName('p')// 死循环for (var i = 0; i < live.length; i += 1) { document.body.appendChild(document.createElement('p'))}JavaScript 作用域
作用域的概念
作用域是一个抽象概念,它描述了一组 binding (绑定) 的生效范围。
我们的程序中的某个部分,某个 name 绑定着某个 entity,而同样的 name,在我们程序的另一个部分,可能绑定了另一个 entity,或者根本不存在这个 name 。
这段话用了几个抽象概念描述作用域的表现形式。即,作用域就是一组 name 跟 entity 的绑定,这种绑定是有起作用的范围的。
解释一下这里用到的几个概念:
- name: 名称,可理解为允许我们引用变量、常量、函数、对象等等等的标识符。
- entity: 实体,可理解为内存中实际的数据、代码等
- binding: 绑定,即 name 跟 entity 之间的关联关系。一个 name 绑定一个 entity,称为引用该对象。
在 JavaScript 中,两个不同的函数内部的同名变量,就完全是不同的东西,有各自的作用范围。 而作用域,正是这种限定变量在程序不同部分的可访问性的概念。
READ MORE...Vue 的一些笔记
Vue 实例化过程关于数据方面的处理
var vm = new Vue({ data: ..., ... })-
使用构造函数的数据初始化实例的
_data属性(即vm._data) -
将
_data属性,使用getter、setter代理到 Vue 实例vm上 即访问、修改vue的属性的时候,只是简单地通过getter、setter访问vm._data属性。 -
观察
vm._data属性(这部分功能交由Observer类进行)
- 生成一个
Observer对象obs,该对象持有vm._data(obs.value === vm._data), 通过递归,将vm._data的数据属性,全部用Object.defineProperty转换成存取器属性 - 针对每个转换后的存取器属性,会实例化一个依赖管理器
dep(Dep类型)对象来维护依赖该属性的观察者(Watcher类型)列表dep.subs。 - 当某个属性的值被修改时,
obs的setter中,会递归地观察新的属性值,并最终让该属性的依赖管理器dep去通知所有watcher(订阅者)执行其update方法(dep.notify(),内部遍历watcher逐个执行watcher.update()) - 每当属性值被读取,会在
getter中,检测Dep.target(Dep类的静态属性)是否有值,有值则是一个Watcher对值的读取(Watcher类的getter中,读取属性的时候,会临时设置Dep.target为watcher实例自身),这时候把该watcher加入当前属性的依赖管理器dep里面,这样该属性值下次改变,该watcher就能获得通知并update
- Vue 实例中,会暴露一个
$watch(expOrFn, cb)接口,主要在指令中使用订阅者,调用该接口,会创建一个Watcher类型的对象,Watcher对象创建过程,会读取vm._data的对应属性,从而触发上述的obs的getter,进而自动将本watcher加入对应属性的依赖管理器dep里,以便在属性下次变化时获得通知有机会执行update方法。
总的来讲:
Observer类
将数据属性转换成存取器属性,并:
- 在每个属性的
getter里面,提供值之前,分析是否是新的watcher,若是,则将其加入对应属性的依赖管理器 - 在每个属性的
setter里面,修改值之后,让依赖管理器去调用所有watcher的update方法
Dep类
用于管理某个 data 的属性的所有订阅者,拥有 notify 方法,该方法在关联的 data 属性发生变化时被调用,内部会执行所有订阅者的 update 方法
Watcher类
用于订阅某个data属性,当属性变化时,执行实例化时构造器中传入的回调函数
Vue类
- 构造时,自动使用
Observer类来转换、维护数据 - 暴露一个
$watch方法,该方法可以生成一个watcher用于订阅data数据
模版编译
编译元素 compileElement(el, options)
- 检查
el的 attributes, - 将 attributes 转成数组 attrs,
- 调用
compileDirectives(attrs, options)进行指令编译,返回一个nodeLinkFn,这是一个链接函数,作用是绑定指令和 Vue 实例 - 将
nodeLinkFn作为最终结果返回
编译指令 compileDirectives(attrs, options)
- 解析每个 attribute,提取修改器(modifier)、过渡效果(transiton)、动态绑定(
v-bind、:),普通指令(v-text、v-ref:xxx、v-el:xxx)等等信息 - 每个成功解析出来的结果,就是一个指令描述(directive descriptor),将所有解析结果推入一个数组待用
// 指令描述数据结构{name: 'text', // 指令名称attr:'v-text', // 指令原始名raw:'值内容', // 指令原始值def: { // 指令定义bind(){},update() {}},arg: arg, // 参数,`v-ref:xxx`中`xxx`部分modifier: modifier, // 修改器expression: parsed && parsed.expression, // 指令原始值中的表达式部分filters: parsed && parsed.filters, // 指令原始值中的过滤器部分interp: interpTokens,hasOneTime: hasOneTimeToken}
- 指令描述数组作为参数,调用
makeNodeLinkFn(directiveDiscriptors),该函数返回一个新函数nodeLinkFn(vm, el, host, scope, frag)nodeLinkFn作为闭包可以访问makeNodeLinkFn的参数directiveDiscriptors,nodeLinkFn执行时为每个 directive discriptor 逐一调用vm._bindDir方法 - 将
nodeLinkFn作为结果返回
Vue.prototype._bindDir(descriptor, node, host, scope, frag)
- 使用
new Directive(descriptor, this, node, host, scope, frag)得到一个Directive实例。 - 将
Directive实例push入 Vue 实例的_directive数组属性里
JavaScript 变量赋值的问题
最近看到有人在讨论一个关于 JavaScript 赋值的问题,代码如下:
var a = {n: 1}var b = aa.x = a = {n: 2}
console.log(a.x) // undefinedconsole.log(b.x) // {n: 2}代码看着有点绕,这个结果很多人表示不能理解。 为了说明这个问题,下面便逐步详细分析。
为了化简式子,下面行文的一些约定:
- 声明环境记录用
ENV表示 {n: 1}用N1表示{n: 2}用N2表示
分析开始:
执行完第一行,声明环境记录 ENV 里面存在绑定:
ENV.a —指向—> {n: 1}(后续用 N1 指代)
执行完第二行,声明环境记录 ENV 里面存在绑定:
ENV.a —指向—> N1
ENV.b —指向—> N1
执行第三行,将三部分分别来看:
- 第一部分
a.x是ENV.a.x,即N1.x - 第二部分
a是ENV.a,即N1 - 第三部分为
{n: 2}(后续用N2指代)
赋值右结合,相当于
N1.x = (ENV.a = N2)
先看第一个赋值动作的执行:
ENV.a = N2
执行后:
ENV.a—指向—>N2ENV.b—指向—>N1- 表达式返回值为
N2
继续第二个赋值动作,将表达式的返回值 N2 赋值给第一部分:
N1.x = N2
此时:
ENV.a—指向—>N2,N1现在为{n: 1, x: N2}ENV.b—指向—>N1- 表达式返回值为
N2
执行 console.log(a.x):
此时 ENV 中的 a 为 N2,相当于:
console.log(N2.x)
而 N2 中没有 x 属性,所以结果:
undefined
执行 console.log(b.x):
此时 ENV 中的 b 为 N1,相当于:
console.log(N1.x)
所以结果为 N2,即 {n: 2}