一、我准备好了
闭包是前端学习中的一个瓶颈,也是它的特色,初学阶段理解起来十分困难,对其应用场景又不熟悉,就连有许多年JavaScript开发经验的人也不一定对闭包真的理解透彻。在接触闭包前请先确认以下五个知识点你已经理解:
1.能说一下什么是引用类型数据吗它与基础类型数据的区别在哪里?
2.请说明一下什么是全局变量?那局部变量呢?
var name = 'coco';
function() {
name = 'ball' // 请问name是全局变量吗?
}
3.请回忆一下 javascript 代码运行机制?请告诉我以下代码执行结果。可否阐述以下 javascript 代码运行时创建阶段做了什么? 执行阶段呢?
function testName(name) {
console.log(name);
console.log(name());
var name = '小明';
function name() {
return '明明不是小明'
}
console.log(name);
console.log(name()); //报错!如果修改上述代码使其不报错能实现吗
}
testName('其实就是小明');
4.请说明一下什么是作用域?再进一步阐述一下函数作用域。
5.这里已经确认的说闭包就是函数作用域里特殊的分支,在扫盲闭包前还需要让你提前知道:
闭包会造成内存泄露?
说这话的人根本不知道什么是内存泄露。内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量明明就是我们需要的变量(lives),凭什么说是内存泄露?
这个谣言是如何来的?
因为 IE。IE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。这是 IE 的问题,不是闭包的问题。
如果你还没准备好,请先回顾一下这些知识点吧 JavaScript 知识扫盲(一)
二、闭包
2.1 闭包来自哪里?
Javascript语言的特殊之处,函数内部可以直接读取全局变量,函数外部却不能读取函数内的局部变量。
2.2 闭包是什么?
函数和函数内部能访问到的变量(也叫环境)的总和,就是一个闭包。
让我们再说的简单点!函数作用内的变量和能访问这个变量的方法构成了一个闭包。
闭包!就是将函数和全局连接起来的一座桥梁,使函数外部能够读取到函数内的变量。
2.3 闭包的详细解释
闭包是一种特殊的对象。
它由两部分组成-执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
许多书籍、文章里都已函数B的名字指代这里生成的闭包。而在Chrome中,则以执行上下文A的函数名指代闭包。
我们只需要知道,一个闭包对象,由A、B共同组成。
2.4 闭包的运用
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
2.4.1 闭包与垃圾回收机制
垃圾回收机制:当一个值失去引用值后就会被标记,然后被垃圾回收机制回收并释放控件。
闭包:闭包本质就是在函数外部保持了内部变量的引用,因此闭包会阻止垃圾回收机制进行回收。
下面我们举一个例子来证明:
function fn() {
var num = 520;
numAdd = function() {
num += 1
}
return function() {
console.log(num);
}
}
var result = fn();
result(); // 520
numAdd();
result(); // 521
首先我们分析闭包,因为 numAdd 和 result 都访问了 fn 中的num,因此他们都与 fn 形成了闭包。这时候变量 num 的引用被保留了下来。而 numAdd 每运行一次就会将 num 的值加一。上述例子就符合这一论证。
2.4.2 闭包与作用域链
闭包的存在不会导致作用域链发生变化。
我们再举个例子巩固闭包知识:
var fn = null;
function foo() {
var a = 1;
function innerFoo() {
console.log(a);
}
fn = innerFoo; // innerFoo引用赋值给全局变量fn
}
function bar() {
fn(); // 此处保留innerFoo的引用
}
foo();
bar(); // 1
在这里详细解释一下函数调用栈和作用域链的区别。函数调用栈是在代码执行时才确定的,而作用域在代码创建阶段就已经确定。虽然作用域链实在代码执行时生成的,但是它的规则不会在执行时发生改变。
所以这里闭包的存在并不会导致作用域链发生变化。
2.4.3 闭包与循环、setTimeout
这里借用一个经典的案例:
//利用闭包知识,修改这段代码,让代码的执行结果为隔秒输出1,2,3,4,5
for(var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 );
}
首先我们回顾一下闭包形成的条件,一个函数里定义一个子函数,子函数内访问函数内的变量对象。因此我们只需要创建出这样的环境即可。
for(var i = 1; i <= 5; i++) {
(function(i) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 )
})(i);
}
定义一个匿名函数,称作A,并将其当作闭包的环境。而timer函数则作为A的内部函数,当A执行时,只需要访问A中的变量对象即可。因此将 i 值作为参数传入,这样也就满足了闭包的条件,并将 i 值保存在了A中。
稍作变通,还有另一种写法
for(var i = 1; i <= 5; i++) {
setTimeout((function(i){
return function timer() {
console.log(i);
}
})(i), i * 1000)
}
2.4.4 闭包与单例模式
设计模式:通常在我们解决问题的时候,很多时候不是只有一种方式,我们通常有多种方式来解决;但是肯定会有一种通用且高效的解决方案,这种解决方案在软件开发中我们称它为设计模式;设计模式并不是一种固定的公式,而是一种思想,是一种解决问题的思路;遵循设计原则可以帮助我们写出高内聚、低耦合的代码,当然代码的复用性、健壮性、可维护性也会更好;在这里先来一个简单的抛砖引玉(S.O.L.I.D.设计原则中的S.O):
-
S(Single responsibility principle)——单一职责原则
一个程序或一个类或一个方法只做好一件事,如果功能过于复杂,我们就拆分开,每个方法保持独立,减少耦合度; -
O(Open Closed Principle)——开放封闭原则
对扩展开放,对修改封闭;增加新需求的时候,我们需要做的是增加新代码,而非去修改源码;
设计模式相关不属于扫盲内容,详细内容会在后续单独整理。言归正传我们继续来说闭包与单例模式。
单例模式核心:确保只有一个实例,并且能够提供给全局访问。
var obj = {
name: 'coco',
age: '3',
getName: function() {
return this.name;
},
getAge: function() {
return this.age;
}
}
这是一个简单的单例模式,一个实例,外部可以访问。但是这样的单例模式有一个严重的问题,即它的属性会被外部修改,极端情况下甚至会被覆盖掉。因此我们期望对象能够拥有自己的私有属性和方法。
下面我用闭包的概念修改一下:
var obj = (function(){
var name = 'coco';
var age = 3;
return {
getName: function() {
return name;
},
getAge: function() {
return age
}
}
})();
obj.getName(); //访问obj的私有变量
对象obj对外能够提供什么样的能力,完全由我们自己决定,我们可以提供一个 getName 方法让外界访问,也可以提供一个 setName 方法让外界修改它。闭包实际的作用我们已经体会到了,下面我们来思考一个这样的问题:
[性能优化] 有的时候(使用频次少)我们希望自己的实例仅仅只是在调用之后才能被实例化,而不是在函数的自执行中就完成了这一操作。
var obj = (function(){
var instance = null;
var name = 'coco';
var age = 3;
function initial() {
return {
getName: function() { return name; },
getAge: function() { return age; }
}
}
return {
getInstance: function() {
instance = instance ? instance : initial();
return instance;
}
}
})();
//在使用时获得实例
var a = obj.getInstance();
//访问私有变量
a.getName();
a.getAge();
我们在匿名函数中定义了一个 instance 变量用来保存实例。在 getInstance 方法中判断了是否对它进行重新赋值。由于这个判断的存在,因此变量 instance 仅仅只在第一次调用 getInstance 方法时赋值了。所以这种写法完美符合了单例模式思路。
2.4.5 闭包与模块化
闭包与模块化 内容已屏蔽,模块化开发采用 ES6 module 相关知识。
实际应用总结:(限于vue-cli版本)
-
在 /src/plugin (vue-cli4.0一下路径可能有区别)路径下新建 .js文件,自己随便起个名(easyJs.js);
-
编辑你的 easyJs.js 文件,function函数正常书写就可以,文件底部加上export {函数名},逗号分隔。
-
怎么使用呢?在需要的.vue后缀文件内加上
import {xxxx,xxxx} from '../plugin/easyJs.js'
即可。
有兴趣还是建议学一下底层实现,之所以vue-cli使用它如此简单是因为,语法糖 + 框架 。
三、this
当前函数的this是在函数被调用执行的时候才确定的
3.1 使用this的原因
让我们回忆一下执行上下文的三大属性:1. 变量对象 2. 作用域 3. this
this提供了一种隐形的传递一个对象的引用,api可以设计的简洁并且灵活。缺点是学习成本高 ,掌握后其实还好
var myself = { name: 'kai duo' }
var yourself = { name: 'big mom' }
function fn() {
return this.name.toUpperCase();
}
function swit() {
var message = 'swit: ' + fn.call(this);
console.log(message);
}
console.log(fn.call(myself)); //KAI DUO
console.log(fn.call(yourself)); //BIG MOM
swit.call(myself); // swit: KAI DUO
swit.call(yourself); //swit: BIG MOM
下面我不用this重写一下上述代码,具体就是给fn(),swit() 传入上下文对象。
var myself = { name: 'kai duo' }
var yourself = { name: 'big mom' }
function fn(context) {
return context.name.toUpperCase()
}
function swit(context) {
var message = 'swit: ' + fn(context)
console.log(message);
}
console.log(fn(myself)); // KAI DUO
console.log(fn(yourself)); // BIG MOM
swit(myself); // swit: KAI DUO
swit(yourself); // swit: BIG MOM
显式的传递让代码异常清晰,让初级程序员少了思考this之苦,但是在工程化大型项目中,显示传递上下文会让代码混乱不堪,使用this避免了这样,这就是为什么this难,但JS设计模式必须使用它的原因!
3.2 对this的错误理解
- this是指向当前函数的本身 ❌
- this 指向的是当前函数的 作用域 ❌
3.3 this与普通对象(object)
var name = 'coco';
var obj = {
name: 10,
getName: function() {
return this.a;
}
}
console.log(obj.getName()); // 10
var test = obj.getName;
console.log(test()); // 20
当前函数的this是在函数被调用执行的时候才确定的,注意分清执行过程中它的调用者分别是谁。
3.4 this与原型(prototype)
function fn() {
}
fn.prototype.name = 'coco';
fn.prototype.getName = function() {
return this.name;
};
fn.prototype.setName = function(message) {
if(message) this.name = message;
};
fn.prototype.delName = function() {
delete this.name;
};
var test = new fn();
console.log(test.getName()); // coco
test.setName('coco ball'); // 原型链实例执行赋值操作
console.log(test.getName()); // coco ball
test.delName(); // 原型链实例删除this.name
console.log(test.getName()); // coco
3.5 [ES6] this与箭头函数
es6箭头函数里的this指的是定义这个函数时外层代码的this,这句话可以从两个方面理解:
- es6箭头函数没有自己的this
- es6箭头函数里的this是外层代码(定义时,非执行时)this的引用
//debugger;
var fn = function() {
this.name = 'coco'; // 下面那一行的this指向的是这外面的this
this.message = (name, words) => {
console.log(this.name + ' is ' + words);
}
}
var test = new fn();
test.message('cococococo', 'ball'); // coco is ball
var message = test.message;
message('cococococo', 'hey~'); // coco is hey~
你可以复制上述代码去浏览器运行它,实时的观察它的输出情况。
总结 : 本篇内容简单梳理了闭包的概念和几种应用场景 以及this关键字的相关内容。下一篇扫盲函数及函数式编程相关基础知识。
上一篇文章: Javascript 知识扫盲 (一)
下一篇文章: Javascript 知识扫盲 (三)