写在开头:作为一名前端工程师,JS对其的重要性就好比是鱼之于水、火之于木、风之于空。咳咳好像有点太文艺了,简单来说JS就是前端这整个大类的基石。我们甚至不需要去像一些程序员那样去争辩“谁才是世界上最好的语言”,也不需要去强调JS的发展前景(毕竟已经是github上最为活跃的语言了)。JS有着各式各样可以被“嫌弃”的理由:动态语言、弱语言、不利于大型项目开发、错误检测机制不够理想、语言风格不够严谨,等等。但是,语言都是人所发明的,总会有些不合理的地方存在,还是那句话相对于争辩或是纠结这些小问题,我们更要做的是在不同的需求场景下,实现较优解。
JS当中的闭包问题及部分作用域概念
想要了解闭包问题,我们首先就需要明白,为什么会出现闭包这一概念,以及闭包这一特性的实际意义。嗯~,说句老实话,我对于闭包的实际应用其实并不算多,可能就只有防抖、节流、柯里化这些小地方。
讲闭包之前,我们先简单地聊聊一个对于闭包来说相当重要的概念:作用域。我们先简单地将作用域区分为全局作用域、局部作用域两种。即所有函数或者脚本都可以使用的处于全局作用域的变量,而处于局部作用域的变量仅能作用于当前函数当中。
在简单了解了作用域之后,我们再回头看闭包这一概念吧,对于JS这门语来说,函数是一等公民这一点是毋庸置疑的,而对于这类“一等公民”,他们不一定是完全的“裸露”在全局环境当中的,总是会有函数嵌套函数的状况出现的。于是乎我们就需要考虑一个问题,那就是在函数嵌套函数的情况下,作用域的问题。
如果只是利用上面的简单总结,那么嵌套在函数当中的函数,能够访问作用在他上层函数中的变量吗?若是能够访问,那该变量还属于处于局部作用域当中吗?在这两个问题抛出的同时,我们就可以继续引入两个新的概念:作用域链与词法作用域。
当然了概念都是人去定义的,没有绝对的对错与绝对的合理。我们要做的是如何深刻的通过个人的方式去理解他。
作用域链:让我们先形象地将变量的作用域比作是一个充满了严格等级分层的阶级。在这个阶级当中,等级高的作用域可以被等级低的函数访问,而等级低的作用域是没有被等级高的函数所访问的价值的(即无法访问)。而在这个等级分层的阶级当中,又会出现同名情况,等级低的函数这时候只能够访问到离他本身等级最近的那个作用域。这样便形成了一个链式的作用域结构,且是单向性的。
词法作用域:还是在这么一个等级森严的阶级当中,在不考虑到某些特权等级(this:动态作用域)的情况下,等级制度是很难被打破的,即从我们写在代码的那一刻,等级制度就已经被建立起来并很难再做改变了。
现在我们就只需要搞清楚什么才算是等级高的、什么是等级低的。嘿嘿,其实再通俗一点地来讲,整个JS的运行环境其实就是一层包裹着一层的嵌套环境,处于外层的自然就是等级高的,而处于嵌套内层的自然也就是等级低的了。
好了,现在结合作用域链与词法作用域这两者,我们再回头来看闭包这一个概念。在这么一个,一经编写便等级森严且上下级关系明确的单向性阶级当中。若是我们突然要改变其等级结构又该怎么办呢?重写代码,或者利用特权等级(this:动态作用域)?这两种好像都是可以的,但是好像都比较麻烦,我们作为一名合格的程序员,自然是应该将问题简单简单再简单化。
有了需求自然就会有解决方案,于是乎,闭包这一个概念就出现了。他不像特权等级一般,能够随意地变化自身的等级,但是他却能够让自己拥有足够的价值,让他值得被等级高的函数访问(即突破作用域链的单向性)。
说的再多,不如来一个?实在。下面是一个很简单的闭包情况。
function demo() {
return function demo1() {
return function demo2() {
var sum = 10;
return sum;
};
};
}
console.log(demo());
console.log(demo()());
console.log(demo()()());
// 第一眼看到demo后面跟着这么多()时,有些不是很了解闭包的人可能会突然脑袋有些懵。
// 但是由于篇幅问题,也无法进行太过详细的解读,只能是单纯的说下结构,大家也可以各自打印一下demo()、demo()()
// demo()运行之后相当于返回了一个demo1的函数,而在demo()()运行时相当于打印了demo2这个函数
// 至于demo()()()也就相当于demo2这个函数被执行了,最终返回的也就是sum、打印结果也就是10
复制代码
看到这个?后,可能会有人觉得毫无意义,经过了这三层循环,好像也就是打印了一个10罢了,为何不直接打印个10出来,难道这就是闭包的意义,这也太鸡肋了吧。想当这里的,其实至少说明了并没有忘记我们程序员的最终宗旨:即为应用场景服务。
再让我们看一下,这个问题的简单变种。嘿嘿,我最喜欢的函数式柯里化要来了,当然这只是最简单的变种。
function demo(a) {
return function demo1(b) {
return function demo2(c) {
var sum = a+b+c;
return sum;
};
}
}
var demoA=demo(1)
var demoAB=demoA(2);
var demoABC=demoAB(3);
console.log(demoABC);
console.log(demo(1)(2)(3)==demoABC);
// 相信看懂了上面那个?的人很快就能够想清楚,该?中前两个的输出结果,分别为6、true。
// 该例子可以简单的被看做是三个数相加的函数运行,而这样的函数运行,明明可以只需要写一层逻辑
// 为什么要使用三层嵌套呢?嘿嘿,我们哪怕先不去扯什么SOLID (面向对象设计)、也不用管什么设计模式的六大原则
// 我们就简单的去想一下实际应用场景。如果说有多个需求都是想要实现三个数的相加,其中又有部分数的重复情况,我们该如何去做
// 若是每一次都将三个参数带入函数当中,是不是感觉会多写不少代码且这不符合我们程序员的“偷懒”特性,于是这种函数式的柯里化就很方便了
// 我们每传递一个参数就相当于一个函数,这样既相互独立且灵活,又互相依赖组装,这样子对于一个三个数的相加函数实际应用场景也更广泛了。
console.log(demo(1)==demoA);
console.log(demo(1)(2)==demoAB);
console.log(demo(1).toString==demoA.toString);
console.log(demo(1)(2).toString==demoAB.toString);
// 嘿嘿,大家可以再想想这四个的打印情况,至于具体原因,涉及简单数据与复杂数据(引用数据)类型。这里就不展开说明了
复制代码
这只是个简单的函数式柯里化雏形,我们再来看一下上一个?中的再次变种,只改变其写法。
function demo(a) {
function demo1(b) {
function demo2(c) {
var sum = a + b + c;
return sum;
}
return demo2;
}
return demo1;
}
console.log(demo(1)(2)(3));
// 这里我们就只是简单的将函数提出来,单独地return出去形成闭包。
// 两者的区别,嗯~,具体还真的没有深究过,应该是没有区别的。
复制代码
最后总结一下,闭包的用处。即提高一些“低等级”的实用价值,让他有资格比高等级访问,不需要拘束于等级森严的JS代码结构。
JS中的this(涉及动态作用域,实则为一个对象)
在等级森严的JS代码世界中,如果只有“闭包”这一种“越级”情况的存在,那么就太对不起JS以灵活著称的语言特性。于是乎,特权等级(this)也就应运而生了,作为特权者(this)他的身份总是飘忽不定的,虽然名字永远是this,可他所代表的等级,却并不是一成不变的,需要我们仔细观察。
那么他的等级(作用域)又是由什么来决定的呢?大家可以简单的理解为,是谁给予的特权等级,this就代表了该等级。即谁最后调用了this,那么this就指向他。
现在我们就可以把问题的重心放到,调用方法,即给予特权等级的方式有几种了。
纯粹的函数调用(函数赋予的特权)
var x=10;
function demo(){
var x=1;
console.log(this.x);
}
demo();
// 这里打印的结果为10,并没有遵循词法作用域,this的作用域确实起作用域了。那么是谁给予的他特权的呢?
// 由结论推导来看,很明显是最高等级(全局环境)给予的特权。
// 而如果我们顺着逻辑来推导的话,该demo函数是在全局环境下调用的,所以便可以看作是全局环境,也可以看出是最高等级给予的其特权。
复制代码
作为对象方法的调用
var x = 10;
function demo() {
var x = 1;
console.log(this.x);
}
var demo1 = Object.create(null);
// 首先创建一个极为纯碎的空对象(无原型),开辟一个新的堆内存空间
console.log(demo1);
// 如果在此刻去打印demo1,可以发现demo1上已经有了x与fn方法(预编译)。当然绝对不建议这样使用。
demo1.x = 2;
demo1.fn = demo;
demo1.fn();
// demo1对象上的fn方法即为demo函数,打印结果为2。同样的用结论推导来看,给予特权者为demo1对象
// 而顺着逻辑来推导的话,给予特权的是demo1对象,所以this所代表的等级即为属于demo1的等级
复制代码
作为构造函数调用
var x = 10;
function demo() {
this.x = 1;
var x = 2;
}
// 我们先设置一个构造函数demo
var demo1 = new demo();
// 然后创建一个demo1对象,并执行构造函数demo,将执行结果的this关键值赋予demo1对象。
// 在new与赋值的过程当中,构造函数就是特权等级的给予者。
console.log(demo1.x);
// 现在让我们来打印构造函数demo中的x,其值很明显为1,
// 至于什么是构造函数,new对象的过程,在这里就不多做详解了,统一放到函数篇讲解。
复制代码
call、apply、bind(this三基友)
var x = 10;
function demo(a) {
var x = 1;
console.log(this.x, a);
}
var demo1 = Object.create(null);
var demo2 = Object.create(null);
// 先创建两个纯粹的无原型的对象
Object.assign(demo2, demo1);
// demo2使用的是demo1的一层浅拷贝方式
var demo3 = JSON.parse(JSON.stringify(demo1));
// demo3采用的则是实践当中最为常用的深拷贝方式,至于赋值、浅拷贝、深拷贝的区别本篇章就不多说明了
demo1.fn = demo;
demo1.x = 2;
demo2.x = 3;
demo3.x = 4;
demo(5);
// 打印值为10,5。其赋予特权者为全局环境
demo.apply(demo1, [6]);
// 打印值为2,6。其赋予特权者为demo1
demo.call(demo2, 7);
// 打印值为3,7。其赋予特权者为demo2
demo.bind(demo3, 8)();
// 打印值为4,8。其赋予特权者为demo3
demo.bind(demo3, [8])();
// 打印值为4,[8]。其赋予特权者为demo3
//让我们简单的总结一下,apply、call、bind作为this的三基友,第一个参数都是赋予特权者,至于第二个参数
// 对于apply来说必须是一个参数数组,call是若干参数列表,bind最为全面,毕竟是后来者,但是需要手动执行。
复制代码
箭头函数(奇妙的this情况,查过一些资料的理解都有些许问题)
var x = 10;
function demo() {
this.x = 2;
var demo1 = () => {
var x = 1;
console.log(this.x);
};
//如果不进行this.x的外层设置,则打印值为10,而如果在箭头函数内部且打印命令之后设置this.x
//则打印值仍然为10,不过现在这种情况下的打印值则是为2
return demo1;
}
var demo2 = Object.create(null);
demo2.fn = demo();
demo2.x = 3;
var demo3 = JSON.parse(JSON.stringify(demo2));
demo3.x = 4;
demo()();
demo2.fn();
demo().apply(demo3);//这里可能有人不能理解,但如果看作是demo函数中demo1函数的绑定,应该就可以理解了
// 这三种方式所打印的值都是2,也就是说对于箭头函数来说,特殊等级的给予者是由外层存在的this来决定的
// 当然了若是直到最外层也不存在this,那么其给予者就是全局环境即windows。
复制代码
易错情况
var x = 10;
function demo() {
console.log(this.x);
}
var demo2 = Object.create(null);
demo2.x = 1;
demo2.fn = demo;
var demo3 = demo2.fn;
demo2.fn();
// 第一个打印值为1,其特殊等级的给予者是demo2对象
demo3();
// 第二个打印值为10,其特殊等级的给予者是全局环境,不要去在意过程中的赋值情况,只需要关注最后的给予者就好。
复制代码
最后说两句:本来还打算将原型及原型链也放在本篇的,但是由于篇幅问题,还是放在下一篇与函数一起来说明吧。其实对于这些最为基本的概念,反而不容易引起大家的注意,甚至有些高赞资料也是有着根本性错误的,希望大家在查找资料的同时,也要有自主的思考能力,当然本系列也是很可能有根本性错误的,若是发现了,希望大大们早早指出。