递归
递归的思想不用再阐述了。JS也可以很轻松的写出递归函数。
function fact(num) {
if (num < 1) {
return 1;
} else {
return num * fact(num - 1);
}
}
高程三这里显得有点皮了,因为函数声明里的函数名也是一个普通的变量,可以被赋值为null,而函数里写死了函数名,如果给函数改了名称,就不能调用了。
fact2 = fact;
fact = null;
console.log(fact2(5))
如果用上面的代码去操作,就得不到正确的结果了,因为内部调用fact这个名字已经失效了。在之前已经知道用 arguments.callee
来调用,不过在严格模式里不生效。高程三给出了一个直接把函数赋给变量的做法。
let fact = (function f(num) {
if (num<1){
return 1
}else {
return num*f(num-1)
}
});
console.log(fact(3));
闭包
在Python的高阶函数的时候已经了解过了闭包,也就函数返回一个在函数内定义的内层函数时,会把内层函数以及携带包含它的函数的作用域一起返回,也即内层函数还可以去访问外层函数的作用域。想要实现闭包,只要用内层函数去访问外层函数的变量就可以了。
作用域链也重新讲了一下,其实就是一个指针数组,每一个指针指向一个保存了那个作用域的全部变量的对象(简称变量对象)。
JS的闭包就是因为作用域链还存在。简单的说,外层函数返回内层函数的时候,内层函数的作用域链包含了外层函数的作用域,只要内层函数还存在,这个作用域链就还存在,自然内层函数可以去访问那个作用域的变量了。
闭包与变量
闭包函数可以访问外层函数作用域的变量,但是,只能访问外层函数的任何变量的最后一个值,不能每次都取出不同的值,因为在作用域链上该变量始终只有唯一的一个。
function any() {
let result = [];
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
};
}
return result;
}
a = any();
console.log(a[0]());
console.log(a[1]());
console.log(a[2]());
console.log(a[3]());
console.log(a[4]());
调用结果数组里的每个函数,结果都是10,如果想要返回每一次循环的变量该怎么办,答案是针对每次变化的i再闭一个包,把传进来的值给内层函数,然后返回内层函数。
function any() {
let result = [];
for (var i = 0; i < 10; i++) {
result[i] = function (num) {
return function (){
return num;
}
}(i)
}
return result;
}
a = any();
console.log(a[0]());
console.log(a[1]());
console.log(a[2]());
console.log(a[3]());
console.log(a[4]());
闭包中this的问题
看一个例子:
var name = "the window3";
// 这里注意,如果用let 定义name,name 不会扩散到window对象。这是let定义的特点。
let obj1 = {
name:"the object",
getName:function() {
return function(){
return this.name;
}
}
};
console.log(obj1.getName()())
结果发现虽然运行环境是在obj1内部,但闭包函数的this并不是obj1,而是window对象。这是因为内部函数被调用的时候,只会在自己的活动对象里搜索this和arguments。匿名函数的执行环境具有全局性,因此一般都指向window对象。
想要正确取得闭包函数的this,必须把当前环境的this传给闭包函数。代码可以修改为:
var name = "the window";
let obj1 = {
name:"the object",
getName:function() {
let that = this;
return function(){
return that.name;
}
}
};
console.log(obj1.getName()())
arguments也是类似的,想要让闭包函数访问外层函数的arguments对象,也做成一个变量传进来就行了。
对于内存泄露的问题,如果确定只需要引用外层环境中的某一个变量或者某一个对象的属性,为了避免每次携带着全部的变量,可以将需要的数据新赋给一个变量保存一个副本,然后只在闭包中使用这个副本,将剩下的不需要的变量在返回闭包之前统统设置成null
块级作用域
由于闭包的特点,只要建立一个闭包函数,引用外部变量并且立刻执行,则执行完之后,闭包内的变量就会被销毁,而同时可以引用外部变量做操作,这就模仿了块级作用域。
现在,则不需要采用此方法,只需要用let来定义块级作用域即可。
for(let i =1;i<10;i++){
console.log(i);
}
console.log(i)
for循环内部的部分会打印出1-9,然而最后一个console.log会显示报错undefined,因为 i 在 for 循环进行完之后已经销毁。
私有变量
像上一章内容建立对象的话,对象的所有属性和方法都是暴露在外的。尤其是方法,JS是没有私有方法的,因为不通过外部引用,无法调用方法。但是函数内部定义的变量是不能够被外界访问的,而闭包只要使用了该变量就可以,所以在类型中定义私有变量,然后用闭包函数作为特权方法访问私有变量。
第一种方法是通过构造函数,注意与普通构造函数的区别:
function My() {
// 直接定义变量,而不是通过this.attr定义属性
let pr = 10;
//直接声明函数,而不是将函数赋给this当做方法
function show() {
return false;
}
//真正赋给this当做方法的是闭包函数,其中引用两个内部定义的变量和函数
this.pm = function () {
pr++;
console.log(pr);
return show();
}
}
my = new My();
这样做完之后,内部的pr变量和show方法只能通过pm方法进行访问。即:将闭包函数赋值给一个公有方法,就可以仅通过该公有方法来操作私有变量。这样就可以隐藏不应该被直接修改的属性。
第二种静态变量方法就类似于类原型的变量,会被所有的实例共享,了解即可。
单例私有变量--模块模式
单例就是指某种类型只有一个实例,比如null类型。模块模式就是为单例创建私有变量和特权方法。
普通情况下,创建单例是用字面量也就是花括号的方式直接创建。
模块模式则是建立一个返回花括号字面量的函数,但是字面量里边引用了函数中定义的方法和属性来达成的:
let singleton = function () {
let pri = 10;
function prifunc() {
return false;
}
return {
public:true,
publicmethod: function () {
console.log(++pri);
return prifunc()
}
}
}();
console.log(singleton.public)
函数返回字面量给变量,构成了单例。但是单例里闭包了函数里的变量。说是单例,实际上这些都是Object的实例,只不过彼此没有关联。通常单例都是存在于全局对象中直接使用,所以一般也无需用instanceOf判断什么类型了。
所谓增强的单例模式,就是这个单例是其他的某种类型的单例,需要增强一些属性,其本质也很简单,本来是返回字面量,现在创建一个所需要的那个类型的对象,然后把闭包方法添加到对象上并且返回对象就可以了。
高程三不愧是好书,将对象和闭包讲的非常透彻。而且Python里的闭包表现形式和JS里一样,引起很多思考。
这里有一个ES6特性,在最开始的闭包例子中,想要保存0-9的结果,使用了再次闭包,在ES6中,如果写成 for(let i=0;i<10;i++)的方式,
,就不需要使用再次闭包,也可以得到0-9。这是ES6 let变量块级作用域的新特性。
还记得JS的三大组成部分吗:ECMAScript,BOM,DOM。
前边的部分已经把JS的ECMAScript部分重新仔细学习了一下,了解了面向过程和JS中的面向对象程序设计。
使用JS绝对不是为了单纯的编写应用程序,而是为了在Web中使用JS,所以剩下来的都是学习BOM,DOM以及为了页面而生的JS要素。