js系列十一:在chrome开发者工具中观察函数调用栈,作用域与闭包

版权声明:版权归作者所有 https://blog.csdn.net/qq_36772866/article/details/87939851

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>

猜你喜欢

转载自blog.csdn.net/qq_36772866/article/details/87939851