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) // 1
console.log(live.length) // 1
// 往`body`里插入一个 `p`
document.body.appendChild(document.createElement('p'))
// 活动的集合会自动刷新,静态的集合不变
console.log(static.length) // 1
console.log(live.length) // 2

live 特性可能带来的问题

因为文档的改变会反应到活动的集合上,所以我们在循环处理一个活动的集合的时候,必须非常小心,不要在循环体上插入符合选择条件的元素,否则可能出现死循环。

例子代码:

var live = document.getElementsByTagName('p')
// 死循环
for (var i = 0; i < live.length; i += 1) {
document.body.appendChild(document.createElement('p'))
}
READ MORE...

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: ..., ... })
  1. 使用构造函数的数据初始化实例的 _data 属性(即 vm._data)

  2. _data 属性,使用 gettersetter 代理到 Vue 实例 vm 上 即访问、修改vue的属性的时候,只是简单地通过 gettersetter 访问 vm._data 属性。

  3. 观察 vm._data 属性(这部分功能交由 Observer 类进行)

  • 生成一个 Observer 对象 obs,该对象持有 vm._data (obs.value === vm._data), 通过递归,将 vm._data 的数据属性,全部用 Object.defineProperty 转换成存取器属性
  • 针对每个转换后的存取器属性,会实例化一个依赖管理器 dep (Dep 类型)对象来维护依赖该属性的观察者(Watcher 类型)列表 dep.subs
  • 当某个属性的值被修改时,obssetter 中,会递归地观察新的属性值,并最终让该属性的依赖管理器 dep 去通知所有 watcher(订阅者)执行其 update 方法(dep.notify(),内部遍历 watcher 逐个执行 watcher.update())
  • 每当属性值被读取,会在 getter 中,检测 Dep.target(Dep 类的静态属性)是否有值,有值则是一个 Watcher 对值的读取(Watcher 类的 getter 中,读取属性的时候,会临时设置 Dep.targetwatcher 实例自身),这时候把该 watcher 加入当前属性的依赖管理器 dep 里面,这样该属性值下次改变,该 watcher 就能获得通知并 update
  1. Vue 实例中,会暴露一个 $watch(expOrFn, cb) 接口,主要在指令中使用订阅者,调用该接口,会创建一个 Watcher 类型的对象,Watcher 对象创建过程,会读取 vm._data 的对应属性,从而触发上述的 obsgetter,进而自动将本 watcher 加入对应属性的依赖管理器 dep 里,以便在属性下次变化时获得通知有机会执行 update 方法。

总的来讲:

Observer类

将数据属性转换成存取器属性,并:

  • 在每个属性的 getter 里面,提供值之前,分析是否是新的 watcher,若是,则将其加入对应属性的依赖管理器
  • 在每个属性的 setter 里面,修改值之后,让依赖管理器去调用所有 watcherupdate 方法

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-textv-ref:xxxv-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 的参数 directiveDiscriptorsnodeLinkFn 执行时为每个 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 数组属性里
READ MORE...

JavaScript 变量赋值的问题

最近看到有人在讨论一个关于 JavaScript 赋值的问题,代码如下:

var a = {n: 1}
var b = a
a.x = a = {n: 2}
console.log(a.x) // undefined
console.log(b.x) // {n: 2}

代码看着有点绕,这个结果很多人表示不能理解。 为了说明这个问题,下面便逐步详细分析。

为了化简式子,下面行文的一些约定:

  1. 声明环境记录用 ENV 表示
  2. {n: 1}N1 表示
  3. {n: 2}N2 表示

分析开始:

执行完第一行,声明环境记录 ENV 里面存在绑定: ENV.a —指向—> {n: 1}(后续用 N1 指代)

执行完第二行,声明环境记录 ENV 里面存在绑定: ENV.a —指向—> N1 ENV.b —指向—> N1

执行第三行,将三部分分别来看:

  1. 第一部分 a.xENV.a.x,即 N1.x
  2. 第二部分 aENV.a,即 N1
  3. 第三部分为 {n: 2}(后续用 N2 指代)

赋值右结合,相当于 N1.x = (ENV.a = N2)

先看第一个赋值动作的执行: ENV.a = N2

执行后:

  1. ENV.a —指向—> N2
  2. ENV.b —指向—> N1
  3. 表达式返回值为 N2

继续第二个赋值动作,将表达式的返回值 N2 赋值给第一部分: N1.x = N2

此时:

  1. ENV.a —指向—> N2N1 现在为 {n: 1, x: N2}
  2. ENV.b —指向—> N1
  3. 表达式返回值为 N2

执行 console.log(a.x): 此时 ENV 中的 aN2,相当于: console.log(N2.x)N2 中没有 x 属性,所以结果: undefined

执行 console.log(b.x): 此时 ENV 中的 bN1,相当于: console.log(N1.x) 所以结果为 N2,即 {n: 2}

READ MORE...