Javascript 知识扫盲(二)

本文深入探讨了JavaScript中的闭包概念及其应用场景,包括闭包如何读取函数内部变量、垃圾回收机制的影响、与作用域链的关系等。同时,解析了this关键字在不同上下文中的行为,如普通对象、原型链和ES6箭头函数中的表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      本文仅记录一些必知的知识理论概念,用以在模棱两可的时候快速回忆相关内容。希望随着知识的应用熟练度提升,尽快将所有必知知识融入到常识之中。废话不多说,详细内容如下:

一、我准备好了

闭包是前端学习中的一个瓶颈,也是它的特色,初学阶段理解起来十分困难,对其应用场景又不熟悉,就连有许多年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版本)

  1. /src/plugin (vue-cli4.0一下路径可能有区别)路径下新建 .js文件,自己随便起个名(easyJs.js);

  2. 编辑你的 easyJs.js 文件,function函数正常书写就可以,文件底部加上export {函数名},逗号分隔。

  3. 怎么使用呢?在需要的.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 知识扫盲 (三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值