Cauil's Blog

陌上花开,可缓缓归矣


  • 首页

  • 归档

  • 标签

JS之异步1

发表于 2017-10-29 | 分类于 JS

什么是异步

异步:一种通讯方式, 一个可以无需等待被调用函数的返回值就让操作继续进行的方法.

在操作系统中对设备的处理,很多就是利用异步来处理的;比如中断,计算机的网卡接受从网络中发过来的数据,如果让cpu一直轮询等待数据的传输完成,这个很费时间, 浪费cpu的效率,
在网卡接受网络数据的时候,可以让cpu自己先忙其他的,等网络数据接受完毕之后才让网卡发起一个中断请求让cpu介入进行处理;这就是异步。

JS中的异步

谈起JS的异步,就不得不说JS引擎是基于单线程事件循环的概念构建的。跟上面举的例子一样,为了让JS更有效率,响应更及时,JS中加入了异步。

JS引擎同一时刻只能执行一个代码块,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列中,每当一段代码准备执行是,都会被添加到任务队列中。每当JS引擎中的一段代码执行结束时,
事件循环会执行队列中的下一个任务。如下图可示:

JS中有哪些异步呢?

setTimeout(), setInterval(), ajax请求,dom事件这些都是JS中比较常见的异步.

事件模型

用户点击按钮或按下键盘上的按钮会触发类似onclick这样的事件,它会向任务队列添加一个新任务来响应用户的操作。如:

1
2
3
4
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("clicked");
};

这段代码中当按钮被点击的时候,会触发onclick事件,执行函数(clickFn)会被添加到任务队列中,当任务队列执行到clickFn的时候打印出clicked;

如下图:

事件模型适用于响应用户交互和完成类似的低频功能,但其对于更复杂的需求来说却不是很灵活。

setTimeout与setInterval

这两个区别不大,我们看一段代码:

1
2
3
4
5
6
7
8
9
setTimeout(function() {
console.log('settimeout');
}, 1000)
var sTime = new Date().getTime();
console.log(new Date());
console.log('test1');
while(new Date().getTime() - sTime < 2000) {
}
console.log(new Date())

执行结果显示打印出:

1
2
3
4
// Sun Oct 29 2017 20:24:29 GMT+0800 (CST)
// test1
// Sun Oct 29 2017 20:24:31 GMT+0800 (CST)
// settimeout

从中可以看出:

  1. 先打印test1,setTimeout的确是异步过程,会在1秒后加入任务队列;
  2. 只有在任务队列中的前面任务执行完,才会执行加入的任务(这段代码是过了两秒);

setTimeout也是一种异步的方式,但是不能很好的把控。

回调模式

回调模式与事件模式类似,只不过回调模式中被调用的函数是作为参数传入的,如下所示:

1
2
3
4
5
6
fs.readFile('example.txt', function(err, contents) {
if(err) {
throw err;
}
console.log(contents)
})

文件example.txt读取完毕后会把回调加入任务队列中,如果此时任务队列为空,直接执行回调,如果不为空,等前面的任务执行完再执行回调;

但是这有一个问题,当我们的需求是,先读取文件1,然后需要把读出的文件1作为内容进行网络转发;那就会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fs.readFile('example.txt', function(err, contents) {
if(err) {
throw err;
};
console.log(contents);
ajax({
url: 'xxx.json'
data: contents,
success: function() {
doSomething()
}
});
})

这样看起来还是,当时如果这样的需求是叠加的,那就可能会形成传说中的回调地狱了。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn1(function(err, result) {
if(err) {throw err;}
fn2(function(err, result) {
if(err) {throw err;}
fn3(function(err, result) {
if(err) {throw err;}
fn4(function(err, result) {
if(err) {throw err;}
})
...
})
})
})

像上面这样嵌套多个方法调用,会创建出一堆难于理解和调试的代码。

异步之生成器

生成器简单介绍

有时间会深入讲述一下生成器,这里只是简单说一下生成器的概念。下面就是一个生成器函数的栗子:

1
2
3
4
5
function *test() { // 生成器函数
yield 1;
yield 2;
}
var iter = test() // 迭代器

上面就是一个简单的生成器函数,当执行会生成一个迭代器,迭代器可参考之前写过的一片文章谈谈遍历与迭代协议.

迭代器每次调用next方法会执行生成器函数,并且每次执行完yield关键词就停止,只在当下一次调用next的时候才回到之前执行停止的地方继续执行;

如若需要回调或者系列化一序列的异步操作,生成器和yield语句就派上用场了。

生成器处理异步操作

任务处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function run(taskDef) {
let task = taskDef(); // 1.外层生成器函数执行 创建一个无使用限制的迭代器
let result = task.next(); // 2. 开始任务,执行到第一个yield
// 循环调用next
function step() {
if(!result.done) { // 3.判断是否迭代完毕(没有yield或者return)
if(typeof result.value === 'function') { // 4.下面包裹函数return的函数
result.value(function(err, data) { // 5.调用return的函数, 传入回调函数到callback
if(err) {
result = task.throw(err);
return;
}
result = task.next(data); // 6.异步回调完,继续执行到下一个yield
step(); // 7. 循环执行
})
}
} else {
result = task.next(result.value);
step(); // 循环执行
}
}
step(); // 启动
}

然后把异步操作包裹起来,例如:

1
2
3
4
5
6
7
let fs = require('sf');
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback)
}
}

下面是最终的调用方式:

1
2
3
4
5
6
run(function *() {
let contents = yield readFile('config.json');
doSomethingWith(contents);
yield readMoreFile('xxx.json');
console.log('Done');
});

这样就可以很好的解决回调地狱的问题了,并且把异步操作通过同步的形式来调用。

小结

上面谈了JS的异步,并分别讲了事件模型/setTimeout/回调模型,最后讲述了怎么用生成器来处理回调地狱,并可以用同步的操作方式.

上面这种处理形式还是比较麻烦,需要把异步函数包裹起来,并且无法区分用作任务执行器回调函数的返回值和一个不是回调函数的返回值。

这种方式在场景使用中还是有局限性,如果需求需要并行执行两个异步操作或者同时进行两个异步操作并且只取优先完成的操作;这种就不太好处理了,会在下一篇中讲述解决的办法.

mongo

发表于 2017-10-19 | 分类于 sql

what is mongo

MongoDB is NoSQL database, NoSQL = not only sql.

And from wikipedia:

The term NoSQL was used by Carlo Strozzi in 1998 to name his lightweight, Strozzi NoSQL open-source relational database that did not expose the standard Structured Query Language (SQL) interface, but was still relational.[16] His NoSQL RDBMS is distinct from the circa-2009 general concept of NoSQL databases. Strozzi suggests that, because the current NoSQL movement "departs from the relational model altogether, it should therefore have been called more appropriately 'NoREL',[17] referring to 'No Relational'.

MongoDB is document database:

  • not .PDF or .DOC/.DOCX
  • is a associative array
  • document == json object
  • document == php array
  • document == python dict
  • document == ruby hash

Why mongo

  • flexible schema
  • oriented toward programmers
  • flexible deployment
  • designed for big data

Install mongo

mac: brew intall mongo

Usage

We can use PyMongo to communicate with mongo in python.

Use pip install PyMongo command to install PyMongo.

And:

1
2
3
from pymongo import MongoClient
client = MongoClient('localhost:27017')
db = client[db_name]

So now we can use db to read/create/update/delete the data.

Hello World

发表于 2017-10-10

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

从尺寸单位到移动端适配方案

发表于 2017-10-01 | 分类于 css

css尺寸单位

绝对单位

在平常的前端开发中,在很多地方写css的时候,你都会用到长度尺寸单位,可能最典型的代表就是px;
其实这些单位主要分两类,绝对单位和相对单位;px就属于绝对单位,除了px之外,其他的你可能都很
少使用,如下:

  • mm, cm, in: 毫米(Millimeters),厘米(centimeters),英寸(inches)
  • pt, pc: 点(Points (1/72 of an inch)), 十二点活字( picas (12 points.))

相对单位

上面列举的主要是绝对单位,那相对单位是啥呢,顾名思义,相对单位就是基于某些参考物的单位;主要有:

  • ex, ch: 分别是小写x的高度和数字0的宽度;
  • em, rem: em和rem都是基于font-size的单位,但又有所不同,em是基于当前元素的font-size为参考物
    的(如果当前font-size没有设置,就基于父元素,以此类推); rem是基于根元素为参考物,如果根元素没有设置,
    大多数浏览器默认为16px;假如参考物的font-size: 20px, 那么1em=20px, 1.5rem=30px;
  • vw, vh, vmin, vmax: 这组单位都是基于视口为参考物,vw基于视口的宽度,vh基于视口的高度,vmin=min(vw, vh),
    vmax=max(vw,vh)

在移动端的使用

目前市面的手机品类繁多,屏幕分辨率也各有不同,导致移动端开发难度加大,我们怎么开发来适配不同的手机?

rem

以iphone三种尺寸手机为例: iphone4(320px),iphone6(375px),iphone6 plus(414px);

普通的做法是根据不同比例的尺寸来等比例
缩放,例如iphone4文本大小是12px,那么iphone6就是(375/320*12px=14px),iphone6 plus就是(414/320*12px=15.5px);

最开始的方法: 以一款尺寸做一版样式,然后用js去控制viewpoint的intial-scale(网页缩放比例); 天猫最初站点就是用这种
方法来进行移动端的开发的,这样有些不好的地方,如手机为大屏幕的时候图片文字会拉伸,导致页面模糊,在某些布局在不同
尺寸的屏幕也有可能导致布局错乱;

为了最好的适配不同的手机尺寸,目前最普遍的做法就是应用到上一节说到的相对单位rem;

兼容性

先来看看rem的兼容性:

caniuse

从图中可以看出兼容性还是很不错的,大部分市面上主流浏览器都支持,我们可以比较放心的使用;

rem官方文档上https://www.w3.org/TR/css3-values/#rem的解释是:

rem unit

rem unit Equal to the computed value of font-size on the root element. When specified on the font-size property of the root element, the rem units refer to the property’s initial value.

实际就是根据根元素来设置字体大小,然后其他设置了rem单位的就是利用这个来进行同等的变换;

适配实现方案

方案1 - 动态添加html样式

1
2
3
4
5
6
(function() {
var b = document.documentElement;
var a = document.createElement("style");
a.innerHTML = "html { font-size: " + ((b.clientWidth > 640 ? 640 : b.clientWidth) / 18.75) + "px !important; }";
b.firstElementChild.appendChild(a)
})();

方案2 - 利用media query来设置

利用css的media query也是可以来实现font-size的设置,如:

1
2
3
@media (min-device-width : 375px) and (max-device-width : 667px) and (-webkit-min-device-pixel-ratio : 2){
html{font-size: 37.5px;}
}

方案3 - js动态设置

1
document.getElementsByTagName('html')[0].style.fontSize = window.innerWidth / 10 + 'px';

rem解决方案升级

上面几个方案我们就实现了动态设置font-size并根据rem实现移动端不同尺寸的适配;

但适用rem的方案还是有不足的地方的:

  1. 大屏幕伸缩之后可能导致页面模糊

  2. 设计稿里面的px都要转换成rem

如何解决这些问题呢?

提供高分辨率设计稿,一般采用的是宽度为750px的设计稿,刚好是375的两倍,并利用sass实现一个函数功能
为提供直接px到rem的转换;

1
2
3
4
@function px2rem($px){
$rem : 75px;
@return ($px/$rem) + rem;
}

设置基准font-size为75,传入参数px大小直接计算出rem的大小;

这样我们就可以完全按照视觉稿上的尺寸来了, 不用除于2了,就解决了图片高清问题,并且不用手动转换单位;

不过rem不是万能的,也还是具有不足之处的,如:

  1. 一个App里native界面和Web界面混合使用时,rem在不同尺寸屏幕上的适配与native界面适配不一致;

  2. rem在多屏幕尺寸适配上与当前两大平台的设计哲学不一致, 有些并不是理想的按照不同大小进行等比例缩放;

总结

本文我们一开始先讲了css尺寸单位,包括绝对单位与相对单位,并详细讲述了em和rem;接下来讲述了rem在现代
移动端开发的应用,最后讲述了rem的几种应用方案与不足之处;

最后祝大家双节快乐,祖国越来越强大!

谈谈遍历与迭代协议

发表于 2017-09-07 | 分类于 js

谈谈遍历与迭代

其实遍历与迭代是意义很近的一组词,但为啥要要把它们都列出来呢?先要从遍历说起:

遍历在js中的形式

The for loop

for循环是最普遍的遍历方式, 下面是一个简单的栗子;

1
2
3
4
5
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (let i = 0; i < digits.length; i++) {
console.log(digits[i]);
}

for循环的不足之处就是必须维护计数器i与退出条件digits.length, 对刚学习编程的人来说,
这可能有点困惑,不能一眼看出这是在做啥;

还有一点是for循环很适合数组类型,但是js不只是只有数组类型,因此for循环不是一个所有类型的解决方案.

The for…in loop

for...in循环优化了for循环的不足之处-需要维护计数器的变化与退出条件;

1
2
3
4
5
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const index in digits) {
console.log(digits[index]);
}

并且这对对象也是适用的,但是这个遍历方式也有一些问题:

正如上面code中,遍历的是计数器,当需要每个value值的时候,需要重新取值digits[index];

并且for…in会把所有的可枚举属性遍历出来,如增加到原型链上的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Array.prototype.decimalfy = function() {
for (let i = 0; i < this.length; i++) {
this[i] = this[i].toFixed(2);
}
}
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const index in digits) {
console.log(digits[index]);
};
// 输出
// 0 1 2 3 4 5 6 7 8 9
/*
function() {
 for (let i = 0; i < this.length; i++) {
  this[i] = this[i].toFixed(2);
 }
}
*/

这也是for…in没有在js程序员中普遍流行的原因;

注意:forEach方法也是js遍历数组的一种方法,forEach实际上是一个数组方法,因而一般
适用在数组上,并且没有方法跳出循环;

for…of

直接看一个栗子:

1
2
3
4
for(let v of 'hello'){
console.log(v);
}
// h e l l o

for…of是针对集合(注意:非set而是collections),而不是所有的对象,适用范围是iterable对象;

什么是iterable对象?

迭代

程序员是一群不停折腾的物种,为了更加方便的遍历,在ES6引入了迭代协议;迭代协议有两种:

  • iterable协议
  • iterator协议

由于这次主题是讲遍历与迭代,所以只涉及第一种协议;

定义

什么是iterable, ECMA官方文档是如下定义的:

Property Value
[Symbol.iterator] A zero arguments function that returns an object, conforming to the iterator protocol.

也就是说只要具有[Symbol.iterator]属性,并且其值是一个0参数函数,这个函数执行返回的是一个符合iterator协议的对象,
符合上述条件的对象就是一个可迭代对象;

1
2
3
var a = []
typeof a[Symbol.iterator] // function
a[Symbol.iterator]() // Array Iterator {}

范围

目前JS内置的可迭代对象有String, Array, Map, Set, TypedArray, arguments等;需要注意的是普通的obj是不能迭代的;

几个用法

遍历:

1
2
3
4
5
var someString = 'hi';
for(let v of something) {
console.log(v);
}
// h i

适用于元素展开:

1
2
3
var set1 = new Set([0, 1, 2]);
var set2 = new Set([2, 3, 4, 5]);
var arr = [...set1, ...set2]; // [0, 1, 2, 2, 3, 4, 5]

转换数组

1
Array.from(new Set(['a', 1, 'b', 2]))

转换成生成器

1
2
3
4
5
6
var arr = [1,2,3];
var iter = arr[Symbol.iterator]();
iter.next(); // {value: 1, done: false}
iter.next(); // {value: 2, done: false}
iter.next(); // {value: 3, done: false}
iter.next(); // {value: undefined, done: true}

leetcode之linked list

发表于 2016-02-21 | 分类于 algorithm

链表javascript实现

链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺
序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度
分别是O(logn)和O(1)。

javascript实现可以通过下面的方法:

function ListNode(val) {
    this.val = val;
    this.next = null;
}

下面我们看看链表题目的基本类型:

删除

删除的题目有19, 83, 203, 237, 82

删除其实就是找到要删除元素的前一个元素pre;

pre.next = pre.next.next;

不过还有一种是假删除,找前一个元素比较麻烦,我们可以直接把要删除元素的下一个元素的值赋给当前删除,然后删除下一个元素;

cur.val = cur.next.val;
cur.next = cur.next.next;

看一个典型例题19. Remove Nth Node From End of List:

Given a linked list, remove the nth node from the end of list and return its head.
For example,

Given linked list: 1->2->3->4->5, and n = 2.
After removing the second node from the end, the linked list becomes 1->2->3->5.

这个题目我们可以通过设置两个变量,fast变量先走n步,然后fast和slow变量同时走,直到fast为null; 此时slow就是到达了倒数第n个元素,执行删除即可;代码如下:

var removeNthFromEnd = function(head, n) {
    var left, before, right = head;
    left = before = {next: head}; 
    while (n--) right = right.next;
    while (right) {
        right = right.next;
        left = left.next;
    }
    left.next = left.next.next;
    return before.next;
};

查找

查找题目有:160, 141, 142

我们来看看141. Linked List Cycle,判断是否🈶️环,我们可以通过设置两个指针fast和slow,slow一次走一步,fast一次走两部,如果🈶️环,fast必定在环内追上slow;于是:

var hasCycle = function(head) {
    var slow = head;
    var fast = head;

    while(fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next;
        if(fast === slow) {
            return true;
        }
    }
    return false;
};

翻转

翻转题目有234,206,24,92,61

234. Palindrome Linked List其实是一个判断是否是回文的题目,但是我们可以利用翻转来判断;

首先利用两个变量来找到中间点,然后分为两个链表,把后面的链表翻转过来,并同时遍历两个链表比较:

var isPalindrome = function(head) {
    function reverse(curr) {
        var prev = null;
        var next;
        while(curr) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
    var fast = head;
    var slow = head;
    while(fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next;
    }
    if(fast) slow = slow.next;

    slow = reverse(slow);
    while(slow && head.val === slow.val) {
        head = head.next;
        slow = slow.next;
    }
    return slow === null;
};

合并

合并的题目有:21,328,86,148,2,92,143

86. Partition List这个题目是让我们把指定元素后面的都小于它的元素放到指定元素前面去;

我们可以设置两个表头,遍历链表,把小于指定元素的链接到第一个表,反之,链接到第二个表,最后把第二个链表链接到第一个链表后面;

var partition = function(head, x) {
    var hd1 = new ListNode(0);
    var hd2 = new ListNode(0);
    var p1 = hd1;
    var p2 = hd2;
    while(head) {
        if(head.val < x) {
            p1.next = head;
            p1 = p1.next;
        } else {
            p2.next = head;
            p2 = p2.next;
        }
        head = head.next;
    }
    p2.next = null;
    p1.next = hd2.next;
    return hd1.next;
};

总结

其实链表的操作就是那几种:删除/查找/翻转/合并, 只要掌握链表的基本操作并配合标记或者特定题目的一些特性,基本都可以解决;

深入理解Javascript: JavaScript核心

发表于 2015-12-27 | 分类于 js

最近本来准备把JS的对象、类、原型继承、作用域链、闭包以及this都写一下的,发现一篇文章写的太好了,就直接转载过来。

作者: JeremyWei | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明

网址: http://weizhifeng.net/javascript-the-core.html

最原始地址:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

0.前言

这篇文章是「深入ECMA-262-3」系列的一个概览和摘要。每个部分都包含了对应章节的链接,所以你可以阅读它们以便对其有更深的理解。

面向读者:经验丰富的程序员,专家。

我们以思考对象的概念做为开始,这是ECMAScript的基础。

1.对象

ECMAScript做为一个高度抽象的面向对象语言,是通过对象来交互的。即使ECMAScript里边也有基本类型,但是,当需要的时候,它们也会被转换成对象。

一个对象就是一个属性集合,并拥有一个独立的prototype(原型)对象。这个prototype可以是一个对象或者null。

让我们看一个关于对象的基本例子。一个对象的prototype是以内部的[[Prototype]]属性来引用的。但是,在示意图里边我们将会使用__<internal-property>__下划线标记来替代两个括号,对于prototype对象来说是:__proto__。

对于以下代码:

var foo = {
  x: 10,
  y: 20
};

我们拥有一个这样的结构,两个明显的自身属性和一个隐含的__proto__属性,这个属性是对foo原型对象的引用:

基本对象

这些prototype有什么用?让我们以原型链(prototype chain)的概念来回答这个问题。

2.原型链

原型对象也是简单的对象并且可以拥有它们自己的原型。如果一个原型对象的原型是一个非null的引用,那么以此类推,这就叫作原型链。

原型链是一个用来实现继承和共享属性的有限对象链。

考虑这么一个情况,我们拥有两个对象,它们之间只有一小部分不同,其他部分都相同。显然,对于一个设计良好的系统,我们将会重用相似的功能/代码,而不是在每个单独的对象中重复它。在基于类的系统中,这个代码重用风格叫作类继承-你把相似的功能放入类 A中,然后类 B和类 C继承类 A,并且拥有它们自己的一些小的额外变动。

ECMAScript中没有类的概念。但是,代码重用的风格并没有太多不同(尽管从某些方面来说比基于类(class-based)的方式要更加灵活)并且通过原型链来实现。这种继承方式叫作委托继承(delegation based inheritance)(或者,更贴近ECMAScript一些,叫作原型继承(prototype based inheritance))。

跟例子中的类A,B,C相似,在ECMAScript中你创建对象:a,b,c。于是,对象a中存储对象b和c中通用的部分。然后b和c只存储它们自身的额外属性或者方法。

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

足够简单,是不是?我们看到b和c访问到了在对象a中定义的calculate方法。这是通过原型链实现的。

规则很简单:如果一个属性或者一个方法在对象自身中无法找到(也就是对象自身没有一个那样的属性),然后它会尝试在原型链中寻找这个属性/方法。如果这个属性在原型中没有查找到,那么将会查找这个原型的原型,以此类推,遍历整个原型链(当然这在类继承中也是一样的,当解析一个继承的方法的时候-我们遍历class链( class chain))。第一个被查找到的同名属性/方法会被使用。因此,一个被查找到的属性叫作继承属性。如果在遍历了整个原型链之后还是没有查找到这个属性的话,返回undefined值。

注意,继承方法中所使用的this的值被设置为原始对象,而并不是在其中查找到这个方法的(原型)对象。也就是,在上面的例子中this.y取的是b和c中的值,而不是a中的值。但是,this.x是取的是a中的值,并且又一次通过原型链机制完成。

如果没有明确为一个对象指定原型,那么它将会使用proto的默认值-Object.prototype。Object.prototype对象自身也有一个__proto__属性,这是原型链的终点并且值为null。

下一张图展示了对象a,b,c之间的继承层级:

原型链

注意: ES5标准化了一个实现原型继承的可选方法,即使用Object.create函数:

var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

你可以在对应的章节获取到更多关于ES5新API的信息。 ES6标准化了 __proto__属性,并且可以在对象初始化的时候使用它。

通常情况下需要对象拥有相同或者相似的状态结构(也就是相同的属性集合),赋以不同的状态值。在这个情况下我们可能需要使用构造函数(constructor function),其以指定的模式来创造对象。

3.构造函数

除了以指定模式创建对象之外,构造函数也做了另一个有用的事情-它自动地为新创建的对象设置一个原型对象。这个原型对象存储在ConstructorFunction.prototype属性中。

换句话说,我们可以使用构造函数来重写上一个拥有对象b和对象c的例子。因此,对象a(一个原型对象)的角色由Foo.prototype来扮演:

// a constructor function
function Foo(y) {
  // which may create objects
  // by specified pattern: they have after
  // creation own "y" property
  this.y = y;
}

// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:

// inherited property "x"
Foo.prototype.x = 10;

// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

// let's show that we reference
// properties we expect

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // also "Foo.prototype" automatically creates
  // a special property "constructor", which is a
  // reference to the constructor function itself;
  // instances "b" and "c" may found it via
  // delegation and use to check their constructor

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

这个代码可以表示为如下关系:

constructor-proto-chain

这张图又一次说明了每个对象都有一个原型。构造函数Foo也有自己的__proto__,值为Function.prototype,Function.prototype也通过其__proto__属性关联到Object.prototype。因此,重申一下,Foo.prototype就是Foo的一个明确的属性,指向对象b和对象c的原型。

正式来说,如果思考一下分类的概念(并且我们已经对Foo进行了分类),那么构造函数和原型对象合在一起可以叫作「类」。实际上,举个例子,Python的第一级(first-class)动态类(dynamic classes)显然是以同样的属性/方法处理方案来实现的。从这个角度来说,Python中的类就是ECMAScript使用的委托继承的一个语法糖。

注意: 在ES6中「类」的概念被标准化了,并且实际上以一种构建在构造函数上面的语法糖来实现,就像上面描述的一样。从这个角度来看原型链成为了类继承的一种具体实现方式:

// ES6
class Foo {
  constructor(name) {
    this._name = name;
  }

  getName() {
    return this._name;
  }
}

class Bar extends Foo {
  getName() {
    return super.getName() + ' Doe';
  }
}

var bar = new Bar('John');
console.log(bar.getName()); // John Doe

有关这个主题的完整、详细的解释可以在ES3系列的第七章找到。分为两个部分:7.1 面向对象.基本理论,在那里你将会找到对各种面向对象范例、风格的描述以及它们和ECMAScript之间的对比,然后在7.2 面向对象.ECMAScript实现,是对ECMAScript中面向对象的介绍。

现在,在我们知道了对象的基础之后,让我们看看运行时程序的执行(runtime program execution)在ECMAScript中是如何实现的。这叫作执行上下文栈(execution context stack),其中的每个元素也可以抽象成为一个对象。是的,ECMAScript几乎在任何地方都和对象的概念打交道;)

4.执行上下文堆栈

这里有三种类型的ECMAScript代码:全局代码、函数代码和eval代码。每个代码是在其执行上下文(execution context)中被求值的。这里只有一个全局上下文,可能有多个函数执行上下文以及eval执行上下文。对一个函数的每次调用,会进入到函数执行上下文中,并对函数代码类型进行求值。每次对eval函数进行调用,会进入eval执行上下文并对其代码进行求值。

注意,一个函数可能会创建无数的上下文,因为对函数的每次调用(即使这个函数递归的调用自己)都会生成一个具有新状态的上下文:

function foo(bar) {}

// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)

foo(10);
foo(20);
foo(30);

一个执行上下文可能会触发另一个上下文,比如,一个函数调用另一个函数(或者在全局上下文中调用一个全局函数),等等。从逻辑上来说,这是以栈的形式实现的,它叫作执行上下文栈。

一个触发其他上下文的上下文叫作caller。被触发的上下文叫作callee。callee在同一时间可能是一些其他callee的caller(比如,一个在全局上下文中被调用的函数,之后调用了一些内部函数)。

当一个caller触发(调用)了一个callee,这个caller会暂缓自身的执行,然后把控制权传递给callee。这个callee被push到栈中,并成为一个运行中(活动的)执行上下文。在callee的上下文结束后,它会把控制权返回给caller,然后caller的上下文继续执行(它可能触发其他上下文)直到它结束,以此类推。callee可能简单的返回或者由于异常而退出。一个抛出的但是没有被捕获的异常可能退出(从栈中pop)一个或者多个上下文。

换句话说,所有ECMAScript程序的运行时可以用执行上下文(EC)栈来表示,栈顶是当前活跃(active)上下文:

当程序开始的时候它会进入全局执行上下文,此上下文位于栈底并且是栈中的第一个元素。然后全局代码进行一些初始化,创建需要的对象和函数。在全局上下文的执行过程中,它的代码可能触发其他(已经创建完成的)函数,这些函数将会进入它们自己的执行上下文,向栈中push新的元素,以此类推。当初始化完成之后,运行时系统(runtime system)就会等待一些事件(比如,用户鼠标点击),这些事件将会触发一些函数,从而进入新的执行上下文中。

在下个图中,拥有一些函数上下文EC1和全局上下文Global EC,当EC1进入和退出全局上下文的时候下面的栈将会发生变化:

ec-stack-changes

这就是ECMAScript的运行时系统如何真正地管理代码执行的。

更多有关ECMAScript中执行上下文的信息可以在对应的第一章 执行上下文中获取。

像我们所说的,栈中的每个执行上下文都可以用一个对象来表示。让我们来看看它的结构以及一个上下文到底需要什么状态(什么属性)来执行它的代码。

5.执行上下文

一个执行上下文可以抽象的表示为一个简单的对象。每一个执行上下文拥有一些属性(可以叫作上下文状态)用来跟踪和它相关的代码的执行过程。在下图中展示了一个上下文的结构:

execution-context

除了这三个必需的属性(一个变量对象(variable objec),一个this值以及一个作用域链(scope chain))之外,执行上下文可以拥有任何附加的状态,这取决于实现。

让我们详细看看上下文中的这些重要的属性。

6.变量对象

变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。

注意,函数表达式(与函数声明相对)不包含在变量对象之中。

变量对象是一个抽象概念。对于不同的上下文类型,在物理上,是使用不同的对象。比如,在全局上下文中变量对象就是全局对象本身(这就是为什么我们可以通过全局对象的属性名来关联全局变量)。

让我们在全局执行上下文中考虑下面这个例子:

var foo = 10;

function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined

之后,全局上下文的变量对象(variable objec,简称VO)将会拥有如下属性:

variable-object

再看一遍,函数baz是一个函数表达式,没有被包含在变量对象之中。这就是为什么当我们想要在函数自身之外访问它的时候会出现ReferenceError。

注意,与其他语言(比如C/C++)相比,在ECMAScript中只有函数可以创建一个新的作用域。在函数作用域中所定义的变量和内部函数在函数外边是不能直接访问到的,而且并不会污染全局变量对象。

使用eval我们也会进入一个新的(eval类型)执行上下文。无论如何,eval使用全局的变量对象或者使用caller(比如eval被调用时所在的函数)的变量对象。

那么函数和它的变量对象是怎么样的?在函数上下文中,变量对象是以活动对象(activation object)来表示的。

7.活动对象

当一个函数被caller所触发(被调用),一个特殊的对象,叫作活动对象(activation object)将会被创建。这个对象中包含形参和那个特殊的arguments对象(是对形参的一个映射,但是值是通过索引来获取)。活动对象之后会做为函数上下文的变量对象来使用。

换句话说,函数的变量对象也是一个同样简单的变量对象,但是除了变量和函数声明之外,它还存储了形参和arguments对象,并叫作活动对象。

考虑如下例子:

function foo(x, y) {
  var z = 30;
  function bar() {} // FD
  (function baz() {}); // FE
}

foo(10, 20);

我们看下函数foo的上下文中的活动对象(activation object,简称AO):

activation-object

并且函数表达式baz还是没有被包含在变量/活动对象中。

关于这个主题所有细节方面(像变量和函数声明的提升问题(hoisting))的完整描述可以在同名的章节第二章 变量对象中找到。

注意,在ES5中变量对象和活动对象被并入了词法环境模型(lexical environments model),详细的描述可以在对应的章节找到。

然后我们向下一个部分前进。众所周知,在ECMAScript中我们可以使用内部函数,然后在这些内部函数我们可以引用父函数的变量或者全局上下文中的变量。当我们把变量对象命名为上下文的作用域对象,与上面讨论的原型链相似,这里有一个叫作作用域链的东西。

8.作用域链

作用域链是一个对象列表,上下文代码中出现的标识符在这个列表中进行查找。

这个规则还是与原型链同样简单以及相似:如果一个变量在函数自身的作用域(在自身的变量/活动对象)中没有找到,那么将会查找它父函数(外层函数)的变量对象,以此类推。

就上下文而言,标识符指的是:变量名称,函数声明,形参,等等。当一个函数在其代码中引用一个不是局部变量(或者局部函数或者一个形参)的标识符,那么这个标识符就叫作自由变量。搜索这些自由变量(free variables)正好就要用到作用域链。

在通常情况下,作用域链是一个包含所有父(函数)变量对象__加上(在作用域链头部的)函数自身变量/活动对象的一个列表。但是,这个作用域链也可以包含任何其他对象,比如,在上下文执行过程中动态加入到作用域链中的对象-像with对象或者特殊的catch从句(catch-clauses)对象。

当解析(查找)一个标识符的时候,会从作用域链中的活动对象开始查找,然后(如果这个标识符在函数自身的活动对象中没有被查找到)向作用域链的上一层查找-重复这个过程,就和原型链一样。

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" and "y" are "free variables"
    // and are found in the next (after
    // bar's activation object) object
    // of the bar's scope chain
    console.log(x + y + z);
  })();
})();

我们可以假设通过隐式的__parent__属性来和作用域链对象进行关联,这个属性指向作用域链中的下一个对象。这个方案可能在真实的Rhino代码中经过了测试,并且这个技术很明确得被用于ES5的词法环境中(在那里被叫作outer连接)。作用域链的另一个表现方式可以是一个简单的数组。利用parent概念,我们可以用下面的图来表现上面的例子(并且父变量对象存储在函数的[[Scope]]属性中):

scope-chain

在代码执行过程中,作用域链可以通过使用with语句和catch从句对象来增强。并且由于这些对象是简单的对象,它们可以拥有原型(和原型链)。这个事实导致作用域链查找变为两个维度:(1)首先是作用域链连接,然后(2)在每个作用域链连接上-深入作用域链连接的原型链(如果此连接拥有原型)。

对于这个例子:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain

console.log(x); // 10

(function foo() {

  // "foo" local variables
  var w = 40;
  var x = 100;

  // "x" is found in the
  // "Object.prototype", because
  // {z: 50} inherits from it

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // after "with" object is removed
  // from the scope chain, "x" is
  // again found in the AO of "foo" context;
  // variable "w" is also local
  console.log(x, w); // 100, 40

  // and that's how we may refer
  // shadowed global "w" variable in
  // the browser host environment
  console.log(window.w); // 20

})();

我们可以给出如下的结构(确切的说,在我们查找__parent__连接之前,首先查找__proto__链):

scope-chain-with

注意,不是在所有的实现中全局对象都是继承自Object.prototype。上图中描述的行为(从全局上下文中引用「未定义」的变量x)可以在诸如SpiderMonkey引擎中进行测试。

由于所有父变量对象都存在,所以在内部函数中获取父函数中的数据没有什么特别-我们就是遍历作用域链去解析(搜寻)需要的变量。就像我们上边提及的,在一个上下文结束之后,它所有的状态和它自身都会被销毁。在同一时间父函数可能会返回一个内部函数。而且,这个返回的函数之后可能在另一个上下文中被调用。如果自由变量的上下文已经「消失」了,那么这样的调用将会发生什么?通常来说,有一个概念可以帮助我们解决这个问题,叫作(词法)闭包,其在ECMAScript中就是和作用域链的概念紧密相关的。

9.闭包

在ECMAScript中,函数是第一级(first-class)对象。这个术语意味着函数可以做为参数传递给其他函数(在那种情况下,这些参数叫作「函数类型参数」(funargs,是”functional arguments”的简称))。接收「函数类型参数」的函数叫作高阶函数或者,贴近数学一些,叫作高阶操作符。同样函数也可以从其他函数中返回。返回其他函数的函数叫作以函数为值(function valued)的函数(或者叫作拥有函数类值的函数(functions with functional value))。

这有两个在概念上与「函数类型参数(funargs)」和「函数类型值(functional values)」相关的问题。并且这两个子问题在”Funarg problem“(或者叫作”functional argument”问题)中很普遍。为了解决整个”funarg problem”,闭包(closure)的概念被创造了出来。我们详细的描述一下这两个子问题(我们将会看到这两个问题在ECMAScript中都是使用图中所提到的函数的[[Scope]]属性来解决的)。

「funarg问题」的第一个子问题是「向上funarg问题」(upward funarg problem)。它会在当一个函数从另一个函数向上返回(到外层)并且使用上面所提到的自由变量的时候出现。为了在即使父函数上下文结束的情况下也能访问其中的变量,内部函数在被创建的时候会在它的[[Scope]]属性中保存父函数的作用域链。所以当函数被调用的时候,它上下文的作用域链会被格式化成活动对象与[[Scope]]属性的和(实际上就是我们刚刚在上图中所看到的):

Scope chain = Activation object + [[Scope]]

再次注意这个关键点-确切的说在创建时刻-函数会保存父函数的作用域链,因为确切的说这个保存下来的作用域链将会在未来的函数调用时用来查找变量。

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" returns also a function
// and this returned function uses
// free variable "x"

var returnedFunction = foo();

// global variable "x"
var x = 20;

// execution of the returned function

returnedFunction(); // 10, but not 20

这个类型的作用域叫作静态(或者词法)作用域。我们看到变量x在返回的bar函数的[[Scope]]属性中被找到。通常来说,也存在动态作用域,那么上面例子中的变量x将会被解析成20,而不是10。但是,动态作用域在ECMAScript中没有被使用。

「funarg问题」的第二个部分是「向下funarg问题」。这种情况下可能会存在一个父上下文,但是在解析标识符的时候可能会模糊不清。问题是:标识符该使用哪个作用域的值-以静态的方式存储在函数创建时刻的还是在执行过程中以动态方式生成的(比如caller的作用域)?为了避免这种模棱两可的情况并形成闭包,静态作用域被采用:

// global "x"
var x = 10;

// global function
function foo() {
  console.log(x);
}

(function (funArg) {

  // local "x"
  var x = 20;

  // there is no ambiguity,
  // because we use global "x",
  // which was statically saved in
  // [[Scope]] of the "foo" function,
  // but not the "x" of the caller's scope,
  // which activates the "funArg"

  funArg(); // 10, but not 20

})(foo); // pass "down" foo as a "funarg"

我们可以断定静态作用域是一门语言拥有闭包的必需条件。但是,一些语言可能会同时提供动态和静态作用域,允许程序员做选择-什么应该包含(closure)在内和什么不应包含在内。由于在ECMAScript中只使用了静态作用域(比如我们对于funarg问题的两个子问题都有解决方案),所以结论是:ECMAScript完全支持闭包,技术上是通过函数的[[Scope]]属性实现的。现在我们可以给闭包下一个准确的定义:

闭包是一个代码块(在ECMAScript是一个函数)和以静态方式/词法方式进行存储的所有父作用域的一个集合体。所以,通过这些存储的作用域,函数可以很容易的找到自由变量。

注意,由于每个(标准的)函数都在创建的时候保存了[[Scope]],所以理论上来讲,ECMAScript中的所有函数都是闭包。

另一个需要注意的重要事情是,多个函数可能拥有相同的父作用域(这是很常见的情况,比如当我们拥有两个内部/全局函数的时候)。在这种情况下,[[Scope]]属性中存储的变量是在拥有相同父作用域链的所有函数之间共享的。一个闭包对变量进行的修改会体现在另一个闭包对这些变量的读取上:

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

以上代码可以通过下图进行说明:

shared-scope

确切来说这个特性在循环中创建多个函数的时候会使人非常困惑。在创建的函数中使用循环计数器的时候,一些程序员经常会得到非预期的结果,所有函数中的计数器都是同样的值。现在是到了该揭开谜底的时候了-因为所有这些函数拥有同一个[[Scope]],这个属性中的循环计数器的值是最后一次所赋的值。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2

这里有几种技术可以解决这个问题。其中一种是在作用域链中提供一个额外的对象-比如,使用额外函数:

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // pass "k" value
}

// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2

对闭包理论和它们的实际应用感兴趣的同学可以在第六章 闭包中找到额外的信息。如果想获取更多关于作用域链的信息,可以看一下同名的第四章 作用域链。

然后我们移动到下个部分,考虑一下执行上下文的最后一个属性。这就是关于this值的概念。

10.This

this是一个与执行上下文相关的特殊对象。因此,它可以叫作上下文对象(也就是用来指明执行上下文是在哪个上下文中被触发的对象)。

任何对象都可以做为上下文中的this的值。我想再一次澄清,在一些对ECMAScript执行上下文和部分this的描述中的所产生误解。this经常被错误的描述成是变量对象的一个属性。这类错误存在于比如像这本书中(即使如此,这本书的相关章节还是十分不错的)。再重复一次:

this是执行上下文的一个属性,而不是变量对象的一个属性

这个特性非常重要,因为与变量相反,this从不会参与到标识符解析过程。换句话说,在代码中当访问this的时候,它的值是直接从执行上下文中获取的,并不需要任何作用域链查找。this的值只在进入上下文的时候进行一次确定。

顺便说一下,与ECMAScript相反,比如,Python的方法都会拥有一个被当作简单变量的self参数,这个变量的值在各个方法中是相同的的并且在执行过程中可以被更改成其他值。在ECMAScript中,给this赋一个新值是不可能的,因为,再重复一遍,它不是一个变量并且不存在于变量对象中。

在全局上下文中,this就等于全局对象本身(这意味着,这里的this等于变量对象):

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

在函数上下文的情况下,对函数的每次调用,其中的this值可能是不同的。这个this值是通过函数调用表达式(也就是函数被调用的方式)的形式由caller所提供的。举个例子,下面的函数foo是一个callee,在全局上下文中被调用,此上下文为caller。让我们通过例子看一下,对于一个代码相同的函数,this值是如何在不同的调用中(函数触发的不同方式),由caller给出不同的结果的:

// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation

function foo() {
  alert(this);
}

// caller activates "foo" (callee) and
// provides "this" for the callee

foo(); // global object
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object

var otherFoo = bar.baz;
otherFoo(); // again global object

为了深入理解this为什么(并且更本质一些-如何)在每个函数调用中可能会发生变化,你可以阅读第三章 This。在那里,上面所提到的情况都会有详细的讨论。

总结

通过本文我们完成了对概要的综述。尽管,它看起来并不像是「概要」;)。对所有这些主题进行完全的解释需要一本完整的书。我们只是没有涉及到两个大的主题:函数(和不同函数之间的区别,比如,函数声明和函数表达式)和ECMAScript中所使用的求值策略(evaluation strategy )。这两个主题是可以ES3系列的在对应章节找到:第五章 函数和第八章 求值策略。

如果你有留言,问题或者补充,我将会很乐意地在评论中讨论它们。

祝学习ECMAScript好运!

作者:Dmitry A. Soshnikov
发布于:2010-09-02

(完)

深入理解Javascript:模块模式

发表于 2015-12-15 | 分类于 js

本文转载自前端乱炖:Javascript 模块模式

1.Javascript 模块模式

假设现在我们有一个小型的Js库,目的是用来增加一个数字:

var jspy = {
  count: 0,

  incrementCount: function() {
    this.count++;
  },

  decrementCount: function() {
    this.count--;
  },

  getCount: function() {
    return this.count;
  }

};

但是,使用这个js库的人可以用jspy.count = 5 的方法来改变这个值。并不是我们的最初目的。在其他的编程语言中你可以定义一个私有变量,但是Javascript并不能“真正”定义私有变量。然而,我们可以通过操作Javascript来实现,这就引出了一个最流行的Javascript设计模式,模块模式。

针对上面问题的解决方案如下:

var jspy = (function() {
  var _count = 0;

  var incrementCount = function() {
    _count++;
  }

  var getCount = function() {
    return _count;
  }
  return {
    incrementCount: incrementCount,
    getCount: getCount
  };

})();

首先我们创造一个_count变量,下划线表明它是一个私有变量。再Javascript中下划线并没有什么实际的意义,但是它是一个用来标明私有变量的普遍用法。现在函数就可以操纵、返回变量了:

然而,你注意到了我吧整个库包含在了一个自调用匿名函数中。这是一个在执行过程中马上被执行的函数。这个函数运行,定义了函数和变量然后到了return {}的部分,它告诉函数将其返回给变量jspy,或者换句话说,暴露给用户。我们暴露两个函数而不是_count变量,这意味着我们可以做如下操作:

jspy.incrementCount();
jspy.getCount();

但是当我们试图进行如下操作时:

jspy._count; //undefined

它返回undefined。

对于上面的这种设计模式有许多不同的实现方法。有人喜欢在return 中定义函数:

var jspy = (function() {
    var _count = 0;

    return {
      incrementCount: function() {
        _count++;
      },
      getCount: function() {
        return _count;
      }
    };
})();   

受到上面例子的启发,CHristian Heilmann提出了Revealing Module Pattern。他的方法是将所有方法定义为私有变量,也就是说,不在return中定义,但是在那里暴露给用户,如下所示:

var jspy = (function() {
  var _count = 0;
  var incrementCount = function() {
    _count++;
  };
  var resetCount = function() {
    _count = 0;
  };
  var getCount = function() {
    return _count;
  };
  return {
    add: incrementCount,
    reset: resetCount,
    get: getCount
  };
})();

这种设计模式有两个好处:

  • 首先,它使我们更容易的了解暴露的函数。当你不在return中定义函数时,我们能轻松的了解到每一行就是一个暴露的函数,这时我们阅读代码更加轻松。

  • 其次,你可以用简短的名字(例如 add)来暴露函数,但在定义的时候仍然可以使用冗余的定义方法(例如 incrementCount)。

相关阅读:阮一峰:Javascript模块化编程(一):模块的写法

深入理解Javascript: 声明提升

发表于 2015-11-19 | 分类于 js

在JS程序中,我们都会需要声明变量和函数,变量声明和函数声明有些什么要注意的呢?

今天就让我们一探究竟。

先看一个例子:

var scope = 'global';
function checkscope(){
    return scope;
    var scope = 'local';
}
var result = checkscope();
console.log(result);

上面例子最后打印出来的result是什么呢,可能有人会说是global,有人会说是local,实际结果却是undifined;

最开始我真是被打击到了,打击过后还是要搞明白啊。

1.作用域

还是先看一个上面例子的变种:

var scope = 'global';
function checkscope(){
    var scope = 'local';
    return scope;
}
var result = checkscope();
console.log(result);

这个可能大家都能说出来是local,是的没错,跟大多数编程语言一样,JS也分全局作用域和函数作用域。

我们先来了解作用域这个概念,在JS中一个变量的作用域是程序源代码中定义这个变量的区域。

全局变量用于全局作用域,在JS代码中的任何地方都是有定义的。然而在函数内的变量只在函数体内有定义。他们是局部变量,作用域是局部的。函数参数也是局部变量,它们只在函数体内有定义。

并且,在函数体内,局部变量的优先级高于同名的全局变量。如果在函数体内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量覆盖。

上面例子的结果就呼之欲出了。

还有一个坑,当我们声明变量时,如果没有加var语句,那么这个变量将会提升为全局变量,如:

var scope = 'global';
function checkscope(){
    scope = 'local';
    return scope;
}
var result = checkscope();
console.log(result); // local

结果变成了local,所以平时声明变量都要加var,不然会造成很多bug,或者原本意义的偏离。

2.变量声明提升

再来看文章开始的例子,为什么会有undefined的结果呢;

原来JS没有块级作用域,JS有函数作用域,变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的,即函数体内的声明的所有变量在函数体内始终是可见的。

有意思的是,这意味着在声明之前甚至可用;JS这个特性被称为变量声明提前,即JS函数内声明的所有变量(不涉及赋值)都被提前至函数体的顶部;这步操作是JS引擎在预编译进行的,是在代码运行之前。

这个例子等价于下面的代码:

var scope = 'global';
function checkscope(){
    var scope;
    return scope;
    scope = 'local';
}
var result = checkscope();
console.log(result);

再来看这个例子,我们就知道由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到var scope = ‘local’时,局部变量才被真正赋值。

所以由于JS没有块级作用域,我们编写程序可以将变量声明放在函数体顶部,这样做法是一个很好的反应真实的变量作用域,减少bug。

3.函数声明提升

老规矩,再来看一个例子:

var foo = 1;
function bar() {
    return foo;
    foo = 2;
    function foo() {return 3;}
    var foo = 4;
}
console.log(bar());

大家会不会说是undefined呢?不是的,这个结果是[function: foo],为什么会出现这种情况呢?

原来函数声明与通过var声明变量一样,函数声明语句中的函数被显式地提前到脚本或函数的顶部,因此它们在整个脚本和函数内部都是可见的。

与变量声明提前不同的是:使用var的话,只有变量声明提前,变量的初始化代码仍然在原来的位置。然而用函数声明语句的话,函数名称和函数体均提前,脚本中的所有函数和函数中所嵌套的函数都会在当前上下文中其它代码之前声明。

为什么要指定是函数声明之前呢,因为使用函数定义表达式创建的函数是不会提前的,见下面的例子:

var foo = 1;
function bar() {
    return foo;
    foo = 2;
    var foo = function foo() {return 3;}
    var foo = 4;
}
console.log(bar()); // undefined

关于函数定义表达式和函数生命表达式的详细内容,以后在深入理解函数中再讲。

深入理解Javascript:DOM事件之事件传播

发表于 2015-10-19 | 分类于 js

客户端Javascript程序采用异步事件驱动编程模型。

那首先什么是异步事件驱动编程模型呢?

在这种设计风格下,当文档、浏览器、元素或与之相关的对象发生某些有趣的事情时,Web浏览器就会产生事件。

这些对象等待事件发生,然后他们相应,这就是所谓的异步驱动风格。

1.事件是什么呢?

事件就是Web浏览器通知应用程序发生了什么事情。

事件类型:是一个用来说明发生什么类型事件的字符串;例如‘mousemove’表示用户移动鼠标,由于事件类型只是一个字符串,有时也可称为事件名字。

事件目标:发生的事件或者与之相关的对象。在JS应用程序中,Windows、Document、Element对象是最常见的事件目标。

下面的代码就是弹出事件目标与类型的方法:

<div id='box'>
    <button>按钮</button>
</div>
<script>
    var bt = document.getElementsByTagName('button')[0];

    bt.addEventListener('click',function(event){
        alert('事件目标:' + event.target);
        alert('事件类型:' + event.type);
    })
</script>

事件处理程序:处理或相应事件的函数。当在特定的事件目标上发生特定类型的事件时,浏览器会调用对应的事件处理程序。

事件对象:与特定事件相关且包含有关该事件详细信息的对象;例如,鼠标事件的相关对象会包含鼠标指针的坐标。

事件传播:浏览器决定哪个对象触发其事件处理程序的过程:

  • 对于单个对象的特定事件(比如windows的load事件),必须是不能传播的;
  • 事件传播向上传播,即冒泡传播;
  • 事件处理程序能够通过调用方法或者设置事件对象属性来阻止事件传播,这样就能停止冒泡;

事件捕获:事件传播的另外一种形式,在容器元素上注册的特定处理程序有机会在事件传播到真实目标之前捕获它。IE8或之前的版本不支持事件捕获,所以不常用它。

2.事件传播

上面介绍了什么是事件,下面详细介绍事件的传播机制;

当事件目标是Windows对象或其他的一些单独对象(如XMLHTTPRequest)时,浏览器简单的通过调用对象上适当的处理程序相应事件。

当事件目标是文档或者文档元素时,情况不同:

事件传播有三个阶段:

捕获阶段,发生在目标处理程序调用之前,称为‘捕获’阶段,为第一阶段;

目标对象本身的时间处理程序调用是第二个阶段;

事件冒泡是事件传播的第三个阶段;

http://7xj29n.com1.z0.glb.clouddn.com/事件传播.jpg

3.事件冒泡

事件最开始由具体的元素(即事件发生的对象)接受,然后逐级向上传播至最不具体的那个节点。

下面是html:

<body>
    <div id='box'>
        <button>按钮</button>
    </div>
</body>

比如当点击上面HTML中的按钮,事件会一直向上冒泡,寻找注册了clink的事件处理程序的元素:

button --> box --> body --> html --> document root

4.事件捕获

事件捕获像反向的冒泡阶段;最先调用windows对象的捕获处理程序,然后是Document对象的捕获处理程序,接着是body对象的,再然后是DOM树向下,依次类推,直到调用事件目标的父元素的捕获事件处理程序。

事件捕获只能用于addEventListener()注册且第三个参数是true的事件处理程序中。意味着事件捕获无法在IE9之前的IE中使用。因为IE9之前的IE中不支持addEventListener。

<body>
    <div id='box'>
        <button>按钮</button>
    </div>
</body>

当点击上面html的按钮时,事件捕获阶段会从上到下寻找注册了click事件捕获处理程序的元素:

document root --> html --> body --> box --> button

5.事件的取消

取消事件的浏览器默认操作有三种方式:

  • 设置事件处理程序的返回值为false;
  • 在支持addEventListener的浏览器中,也能通过调用事件对象的preventDefault()方法取消事件的默认行为
  • 在IE9之前的IE中,可以设置事件对象的returnValue属性为false

取消事件的传播可以通过:

  • 在支持addEventListener的浏览器中,也能通过调用事件对象的一个stopPropagation()方法取消事件的继续传播;如果在同一对象上定义了其他处理程序,剩下的处理程序将依旧被调用,但调用stopPropagation方法之后其他任何对象的事件处理程序将不会调用
  • IE9之前不支持上一种方法的可以通过IE事件对象的cancelBubble属性设置为true能阻止事件进一步冒泡(这些版本事件传播不支持事件的捕获阶段)

6.综合例子

下面是一个综合例子:

当点击按钮时,会依次弹出对话框:‘事件捕获阶段body’、‘事件捕获阶段box’、‘事件捕获阶段:button’、‘事件调用阶段:button’、‘事件冒泡阶段:box’、‘事件冒泡阶段:body’;

当点击box时,会依次弹出对话框:‘事件捕获阶段body’、‘事件捕获阶段box’、‘事件冒泡阶段:box’、‘事件冒泡阶段:body’;

<html>
    <head>
        <title>事件流</title>
        <meta charset="utf-8">
        <style>
            #box {height:100px;width: 100px;border: solid 1px red; text-align: center;background-color: burlywood}
            #button {border: solid 1px red;}
        </style>
    </head>

    <body id="body">
        <div id="box">box
            <button id="button">按钮</button>
        </div>
        <script>
            var bt = document.getElementById('button');
            var bx = document.getElementById('box');
            var bd = document.getElementById('body');

            bt.addEventListener('click', function(){
                alert("事件捕获阶段:button");
            }, true);
            bx.addEventListener('click', function(){
                alert("事件捕获阶段box");
            },true);
            bd.addEventListener('click', function(){
                alert("事件捕获阶段body");
                // event.stopPropagation();
            },true);

            bt.addEventListener('click', function(){
                alert("事件调用阶段:button");
            }, false);
            bx.addEventListener('click', function(){
                alert("事件冒泡阶段:box");
            },false);
            bd.addEventListener('click', function(){
                alert("事件冒泡阶段:body");
            },false);
        </script>
    </body>

</html>
1234
cauil

cauil

37 日志
12 分类
23 标签
GitHub Weibo DouBan
© 2018 cauil
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.3