1 基础概念的回顾
函数在被调用执行时,会创建一个当前函数的执行上下文,在该执行上下文的创建阶段,变量对象,作用域链,闭包,this 会分别确认,而一个程序中一般来说会有多个函数执行,因此执行引擎会使用函数调用栈来管理这些函数的执行顺序。函数调用栈的执行顺序与栈数据结构一致。
2 认识断点调试工具
浏览器右上角竖着的三点 -> 更多工具 -> 开发者工具 -> Sources
断点调试界面
在我的demo中,我把代码放在app.js中,在index.html中引入。我们暂时只需要关注截图中红色箭头的地方。在最左侧上方,有一排图标。我们可以通过使用他们来控制函数的执行顺序。从左到右他们依次是:
- resume/pause script execution
- 恢复/暂停脚本执行
step over next function call - 跨过,实际表现是不遇到函数时,执行下一步。遇到函数时,不进入函数直接执行下一步。
step into next function call - 跨入,实际表现是不遇到函数时,执行下一步。遇到到函数时,进入函数执行上下文。
step out of current function - 跳出当前函数
deactivate breakpoints - 停用断点
don‘t pause on exceptions
不暂停异常捕获
其中跨过,跨入,跳出是我使用最多的三个操作。
上图左侧第二个红色箭头指向的是函数调用栈(call Stack),这里会显示代码执行过程中,调用栈的变化。
左侧第三个红色箭头指向的是作用域链(Scope),这里会显示当前函数的作用域链。其中Local表示当前的局部变量对象,Closure表示当前作用域链中的闭包。借助此处的作用域链展示,我们可以很直观的判断出一个例子中,到底谁是闭包,对于闭包的深入了解具有非常重要的帮助作用。
实例
var fn;
function foo() {
var a = 20;
function baz() {
console.log(a);
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar();
很显然,fn是对foo或者foo内部函数baz的引用,因此fn执行时,其实就是baz执行。而baz在执行时访问了foo中的变量,因此闭包产生。在chrome中,用foo来指代生成的闭包。
function foo() {
var x = 20;
var y = 10;
function child() {
var m = 5;
return function add() {
var z = "this is add";
return x + y;
}
}
return child();
}
foo()();
在上面我们使用学习过的知识 变量对象 来思考一下函数 add 在它执行时他的作用域链的应该是怎么样的情况?
addEC = {
scopeChian: [AO(add), VO(child), VO(foo), VO(global)]
}
下面再来看一个例子
function foo() {
var a = 10;
function fn1() {
console.log(a);
}
function fn2() {
var b = 10;
console.log(b)
}
fn2();
}
foo();
还是产生了闭包,有的同学就有疑问了,fn1函数没有调用啊,怎么就产生闭包了呢?
其实就是在最新的MDN中,对闭包是这样定义的:“闭包就是这样的引用包含了一个函数(fn1),这个函数(fn1)可以被调用这个作用域所封闭的变量(a),函数,或者闭包等内容。”通常我们通过闭包所对应的函数来获取对闭包的访问
闭包的应用
循环,setTimeout,与闭包
在面试题中,常常会遇到一个与循环,闭包有关的问题
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},i*1000);
}
首先分析一下如果直接运行这个例子会输出什么结果。
前面我们已经知道,for 循环的大括号并不会形成自己的作用域,因此这个时候肯定是没有闭包产生的,而 i 值作为全局的一个变量,会随着循环的过程递增。因此循环结束之后,i 变成 6.
而每一个循环中,setTimeout的第二个参数访问的都是当前 i 的值,因此第二个 i 的值分别是1,2,3,4,5, 而参数timer函数中虽然访问的都是同一个值,但是由于延迟的原因,当前timer 函数被setTimeout运行时,循环结束,即 i 已经变成了 6;
因此这段代码输出的结果都是 6
而我们想要的隔秒输出1,2,3,4,5,因此我们需要借助闭包的特性,将每一个 i 值都用一个闭包保存起来,每一轮循环,都把当前 i 值保存下来一个闭包中,当前setTimeout中定义的操作执行时,访问对应的闭包。
for(var i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i)
},i * 1000)
})(i)
}
定义一个匿名函数,称作A,并将其当作闭包的环境,而timer函数则作为A的内部函数,当A执行时,只需要访问A的变量即可,因此将 i 值作为参数传入,这样也就满足不了闭包的条件,并将 i 值闭包保存了A中。
同样的道理,也就是可以在 匿名函数里面做文章。
for(var i=0;i<5;i++){
setTimeout((function(i){
console.log(i);
})(i), i * 1000);
}
单例模式与闭包
1 最简单的单例模式
对象字面量的方法就是最简单的单例模式,我们将属性与方法依次存放在字面量里面。
var per = {
name: 'Jake',
age: 20,
getName: function() {
return this.name;
},
getAge: function() {
return this.age;
}
}
但是这样的单例模式有一个问题, 即他的属性可以被外部修改。因此在许多场景中,这样的写法并不会符合我们的需求,我们期望对象拥有自己的私有方法与属性。
2 有私有方法、属性的单例模式
想要一个对象拥有私有方法属性,那么只需要创建一个单独的作用域将对象与外界隔离起来就行了。这里我们借助匿名函数自执行的方法即可。
var per (function() {
var name = "Jake";
var age = 18;
return {
getName: function() {
return name;
},
getAge: function() {
return age;
}
}
})();
per.getName()//访问私有变量
私有变量的好处在于,外界对于私有变量能够进行什么样的操作是可以控制的。我们提供一个getName方法让外界可以访问名字,也可以额外提供一个setName方法,来修改他的名字,对外提供这种什么样的能力,完全由我们决定。
3 调用时才初始化的单例模式
有时候(使用频次较少)我们希望自己的实例仅仅只是在调用的时候才能被初始化,而不能如上面两个例子那样,即使没有调用per,per的实例在函数自执行的时候就会返回了。
var per = (function() {
var instance = null;
var age = "Jake";
var age = 18;
//初始化方法
function initial() {
return {
getName: function() {
return name;
},
getAge: function() {
return age;
}
}
}
return {
getInstance: function() {
if(!instance){
instance = initial();
}
return instance;
}
}
})()
// 只在使用时获取实例
var p1 = per.getInstance();
var p2 = per.getInstance();
console.log(p1 === p2); // true
在这个例子中,我们在匿名函数中定义了一个instance变量来保存实例,在getInstance方法中判断了是否对他进行重新赋值。由于这个判断的存在,因此变量instance仅仅只在第一步调用getInstance方法时赋值了。
模块化与闭包
如果想在所有的地方都能够访问同一个变量,那么应该怎么办呢?在实践中这种场景很多,比如全局状态管理。
但是前面我们介绍过,在实际开发中,不要使用全局变量,那又该怎么办呢?
模块化思维帮助我们解决这个问题。
模块化开发是目前最流行的,也是必须要掌握的一个开发思路,而模块化其实是建立在单例模式基础之上的,因此模块化开发与闭包息息相关。
第一:请记住:每一个单例就是一个模块。
在未来,你可能会被告知,每一个文件,就是一个模块。而这里把每一个单例模式假想成一个单独的文件即可,而变量就是模块名。
var module_test = (function(){
})();
第二:每一个模块想要与其他模块交互,则必须有获取其他模块的能力。例如require.js中的require与es6中的import
// require
var $ = require("jquery");
// ES6 module
import $ from "jquery";
第三:每一个模块都应该对外有接口,以保证与其他模块交互的能力,这里直接使用return,返回一个字面量对象的方式来对外提供接口。
var module_test = (function(){
...
return {
testfn1: function(){},
testfn2: function(){}
}
})();
现在我们结合一个简单的例子来走一遍模块化开发流程。
(1)首先创建一个专门用来管理全局状态的模块,这个模块中有一个私有变量保存了所有的状态值,并对外提供访问与设置私有变量的方法。
var module_status = (function(){
var status = {
number: 0,
color: null,
}
var get = function(prop){
return status[prop]
}
var set = function(prop,value){
status[prop] = value;
}
// 暴露接口
return {
get: get,
set: set
}
})();
(2)再来创建一个模块,这个模块专门负责body背景颜色的改变。
var module_color = (function(){
// 假装可以使用一下这种方式引入模块
// 类似于 import state from "module_status";
var staus = module_status;
var colors = ["orange", "#ccc", "pink"];
function render() {
var color = colors[status.get('number') % 3];
document.body.style.backgroundColor = color;
}
return {
render: render
}
})();
接下来我们还要创建另外一个模块来显示当前的number值,用于参考与对比
var module_number = (function(){
var status = module_status;
function render() {
document.body.innerHTML = "this Number is " + staus.get("number");
}
return {
render: render
}
})();
这些功能模块都创建完毕之后,最后我们只需要创建一个主模块即可,这个主模块的目的就是借助功能模块,来实现我们想要的结果。
var module_main = (function(){
var state = module_status;
var color = module_color;
var number = module_number;
setInterval(function(){
},1000);
})();
把下面这段代码摘抄到一个HTML文件中,即可展示效果,
<script>
var module_status = (function(){
var status = {
number: 0,
color: null,
}
var get = function(prop){
return status[prop]
}
var set = function(prop,value){
status[prop] = value;
}
// 暴露接口
return {
get: get,
set: set
}
})();
var module_color = (function(){
// 假装可以使用一下这种方式引入模块
// 类似于 import state from "module_status";
var staus = module_status;
var colors = ["orange", "#ccc", "pink"];
function render() {
var color = colors[status.get('number') % 3];
document.body.style.backgroundColor = color;
}
return {
render: render
}
})();
var module_number = (function(){
var status = module_status;
function render() {
document.body.innerHTML = "this Number is " + staus.get("number");
}
return {
render: render
}
})();
var module_main = (function(){
var state = module_status;
var color = module_color;
var number = module_number;
setInterval(function(){
},1000);
})();
</script>