关于闭包的一些实践与应用
闭包是什么
- 闭包让你可以在一个内层函数中访问到其外层函数的作用域
- 闭包出现的原因是因为作用域链的存在,使得内部的函数可以有一条作用域链指向外部函数以及外部环境
- 主要有两个使用场景
- 创建私有变量
- 延长变量的生命周期 -> 一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
闭包的的经典问题
闭包的作用域问题
let a = 'glows777';
function foo() {
let a = 'glows';
function fo() {
console.log(a);
}
return fo;
}
function f(func) {
let a = '我来了';
func();
}
f(foo());
- 在函数foo()中,return了一个函数fo(),所以,fo()是闭包,那么,f(foo())传入的参数就是函数fo(),因为fo()的上级作用域就是foo(),所以输出'glows'
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
f()
return f
}
var x = fn()
x()
x()
console.log(n)
- 第1,2,3的输出,因为存在闭包,所以访问的都是用一个n,所以会叠加
闭包中的this
- this对象是运行时基于函数的执行环境绑定的。全局函数中,this指向 window,当函数被作用某个对象的方法调用时,this指向这个对象,不过匿名函数的执行环境具有全局性,因此其this对象通常指向window
var name = 'window';
let obj = {
name: 'obj',
getName: function() {
return function() {
return this.name;
}
}
}
console.log(obj.getName()());
var name = 'window';
let obj = {
name: 'obj',
getName: function() {
let that = this;
return function() {
return that.name;
}
}
}
console.log(obj.getName()());
- 情况1,为什么匿名函数没有取得包含作用域的this对象,?每个函数在被调用时会自动获取两个特殊的变量: this, arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量
- 情况2,把外部作用域中的 this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,此时闭包return出去后,that还是指向obj这个对象,所以返回obj
- 永远记住,this指向作用域!!!
柯里化函数
- 对于已经柯里化后的 _fn 函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数; 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数
- 一般,柯里化用于提高函数的自由度,比如在封装验证电话号码,邮箱等
function checkByRegExp(regExp, string) {
return regExp.test(string);
}
checkByRegExp(/^1\d{10}$/, '18642838455');
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
checkByRegExp(/^1\d{10}$/, '18642838455');
checkByRegExp(/^1\d{10}$/, '13109840560');
checkByRegExp(/^1\d{10}$/, '13204061212');
let _check = currying(checkByRegExp);
let checkPhone = _check(/^1\d{10}$/);
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
checkPhone('18642838455');
checkPhone('13109840560');
checkPhone('13204061212');
checkEmail('test@163.com');
checkEmail('test@qq.com');
checkEmail('test@gmail.com');
如何实现
- 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
- 在调用柯里化工具函数时,手动指定所需的参数个数
function currying(func, len = func.length, holder = currying) {
return _curry.call(this, func, len, holder, [], []);
}
function _curry(func, len, holder, args, holders) {
return function (..._args) {
let params = args.slice();
let _holders = holders.slice();
_args.forEach((arg) => {
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index), 1);
params[index] = arg;
} else if (arg !== holder && !holders.length) {
params.push(arg);
} else if (arg === holder && !holders.length) {
params.push(arg);
_holders.push(params.length - 1);
} else if (arg === holder && holders.length) {
holders.shift();
}
});
if (params.length >= len && params.slice(0, len).every(i => i != holder)) return func.apply(this, params);
else return _curry.call(this, func, len, holder, params, _holders);
};
}
const myPrint = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
};
let _ = {};
let _myPrint = currying(myPrint, 5, _);
_myPrint(1, 2, 3, 4, 5);
_myPrint(1, 2, 3, 4, _)(5);
_myPrint(1, 2, 3, _, 5)(4);
_myPrint(1, _, 3)(_, 4,_)(2)(5);
模拟私有变量
- JS本质上是没有私有成员的概念,所有的对象属性都是公有的,不过,有私有变量的概念
- 可以说,任何定义在函数或者块,外部无法访问的变量,都是私有变量,包括函数参数,局部变量,函数内部定义的其他函数等
ES5实现私有变量
function Person(name) {
this.getName = function () {
return name;
};
this.setName = function (newName) {
name = newName;
};
}
let person = new Person("John");
console.log('person1 ', person.getName());
person.setName("Jane");
console.log('person1 ' ,person.getName());
let person2 = new Person("James");
console.log('person2: ' ,person2.getName());
person2.setName("Jim");
console.log('person2: ' ,person2.getName());
person.setName("Jimmy");
console.log('person1: ' ,person.getName(), 'person2: ' ,person2.getName());
(function () {
let name = "";
let age = "";
function privateMethods() {
return false;
}
Person = function (value) {
name = value;
};
Person.prototype.setAge = function (value) {
age = value;
};
Person.prototype.getAge = function () {
return age;
};
Person.prototype.getName = function () {
return name;
};
Person.prototype.setName = function (newName) {
name = newName;
};
})();
let person1 = new Person("张三");
person1.setAge(18);
console.log(person1.getName(), person1.getAge());
person1.setName("李四");
console.log(person1.getName());
let person2 = new Person("王五");
console.log(person2.getName(), person2.getAge());
person2.setAge(20);
console.log('person1: ', person1.getAge(),'person2: ',person2.getAge());
- 非静态的私有变量,对于每一个实例而言,都是独一无二的,每次调用构造函数都会重新创建一套变量和方法,这样每个实例都是独立的属性,会比较耗费资源、
- 静态私有变量,是全局的,所有实例共享这个变量,修改一个会影响所有,主要的实现思路是,用匿名函数表达式创建一个包含构造函数和方法的私有作用域,,然后声明Person没有使用任何关键字,因为不是用关键字创建的变量会创建在全局的作用域,这样就会使得Person变成全局变量,可以在这个私有作用域外部被访问。(非严格模式)
ES6实现私有变量
- 分别有约定写法,闭包写法,Symbol写法,WeakMap写法以及新提案写法
约定写法
- 主要是约定通过下划线(_XXX)来标识为私有变量
- 缺点:本质上,内外部都可以访问得到该变量,而且通过for in会将所有属性枚举出来
class A {
constructor() {
this._name = 'glows777'
}
getName() {
return this._name;
}
}
const a1 = new A();
console.log(a1.getName());
console.log(a1._name);
闭包写法
- 本质上,也是和ES5实现的方法一样,也有两种类型(静态和非静态),只不过是结合了class语法糖
非静态私有变量
- 缺点
- 挂载在实例上而非原型链上,浪费资源,无法通过super关键字调用
- constructor 的逻辑变得复杂。构造函数应该只做对象初始化的事情,现在为了实现私有变量,必须包含部分方法的实现,代码组织上略不清晰
class A {
constructor() {
let _name = 'glows777';
this.getName = function() {
return _name;
}
}
}
const a1 = new A();
console.log(a1._name);
console.log(a1.getName());
静态私有变量
const A = (function() {
let _name = '';
class helper {
constructor(name) {
_name = name;
}
getName() {
return _name;
}
}
return helper;
} ());
const a1 = new A("glows777");
console.log(a1._name);
console.log(a1.getName());
const a2 = new A("Liam");
console.log(a2.getName());
console.log(a1.getName());
Symbol写法
- 缺点:
- 写法稍微复杂,但是无性能损失
- 可能被Object.getOwnPropertySymbols()方法获取到,所以,并不完全是私有变量
const A = (function() {
let _name = Symbol('name');
class helper {
constructor(name) {
this[_name] = name;
}
getName() {
return this[_name];
}
}
return helper;
} ());
const a1 = new A("glows777");
console.log(a1._name);
console.log(a1.getName());
const a2 = new A("Liam");
console.log(a2.getName());
console.log(a1.getName());
let b = Object.getOwnPropertySymbols(a1);
console.log(a1[b[0]]);
WeakMap写法
const A = (function() {
let _privateStorage = new WeakMap();
class helper {
constructor(name) {
_privateStorage.set(this, { name });
}
getName() {
return _privateStorage.get(this).name;
}
}
return helper;
} ());
const a1 = new A("glows777");
console.log(a1._name);
console.log(a1.getName());
const a2 = new A("Liam");
console.log(a2.getName());
console.log(a1.getName());
新提案
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(point) {
return this.#x === point.#x && this.#y === point.#y;
}
}
由于个人疏忽原因,学习以上内容时忘记标记学习的文章,博客出处,因此只能在文章结尾对网上各位大佬输出的博客致以感谢和歉意😭😭😭