JavaScript高级程序设计—第七章函数表达式

1.函数表达式和函数声明

函数声明
if(flag) {
  function sayHello() {
    console.log(`Hello dayday!`);
  }
}
else {
  function sayHello() {
    console.log(`Hello huahua!`);
  }
}

sayHello() // Hello huahua!

在上面例子中,我们希望根据flag的值来创建一个函数,即如果flag==true则声明一个sayHello函数,打印字符串"Hello dayday!",当flag==false则声明一个sayHello函数,打印字符串"Hello huahua!"。然而实际情况却不是我们想象那样,不管flag为true还是false,都会打印Hello huahua!。这是为什么呢?因为在ES5标准中,使用函数声明的方式来声明一个函数,此函数声明会被提升到作用域的头部,而重复声明一个函数时,后面的函数声明会覆盖前面的函数声明,因此不管flag值为true还是false,sayHello函数都是最后声明的那个。
但是注意,对于新版本的浏览器,最后会返回Hello dayday!。这是因为新版本浏览器中不会将整个函数体全部提升,而是只提升函数名。这样相当于将函数声明转换成了函数表达式:

函数表达式
if(flag) {
  sayHello = function() {
    console.log(`Hello dayday!`);
  }
}
else {
  sayHello = function() {
    console.log(`Hello huahua!`);
  }
}

sayHello() // Hello dayday!

上面示例时使用函数表达式和在新版本中运行的结果,将一个匿名函数赋值给sayHello变量,在执行时就会根据flag值不同对sayHello变量赋不同的值。

2.递归:递归函数即在一个函数执行过程中,通过名字调用自身的情况下构成的

let recursion2 = recursion;
recursion = null;
console.log(recursion2(4)) // error,recursion is not a function

上面示例中,在recursion函数体内通过函数调用自身,这就是经典的递归调用。第一调用recursion(4)时返回结果1,但是第二次调用recursion(4)却报错了,这是因为我们将recursion变量赋值给了recursion2变量,接着给recursion赋值为null,此时recursion变量已经不再是指向原始函数的引用了,因此在函数体内无法再通过recursion函数名来调用自身了,导致代码出错。
解决这种情况的方法有两种,一是在递归函数体内,通过arguments.callee()来调用自身(在严格模式下使用arguments.callee会报错),另一种是是通过命名函数表达式的方法调用自身:

函数赋值给变量recursion
let recursion =  (function tempFn(num) {
  if(num <= 1) {
    return 1
  }
  else {
    return tempFn(num-1);
  }
})

let recursion2 = recursion;
recursion = null;
console.log(recursion2(4)); // 1

上面示例将一个名为tempFn的函数赋值给了变量recursion,然后在函数体内通过函数调用自身,此时内部调用不再依赖变量recursion,因此可以给recursion变量赋任意值。

3.闭包:闭包是指可以访问另一个函数作用域中变量的函数,创建闭包最常见的方法就是在函数内部创建一个函数并返回。

let calculate = createClosure(5);
let result1 = calculate(4);
let result2 = calculate(4);
console.log(result1) // 9
console.log(result2) // 13

上面示例中就创建了一个闭包,createClosure函数的返回值是一匿名函数调用createClosure()函数并将其赋值给一个变量calculate,此时calculate也就是createClosure中返回的匿名函数,我们可以通过执行语句calculate(4);直接调用函数,calculate函数内部使用了createClosure函数中的局部变量num,然后将num与传入的参数相加并返回结果。
执行上面的代码我们可以看到,尽管传入相同的参数,但是两次调用calculate函数得到的值却是不相等的,为什么呢? 这就是闭包的特性,正常情况下函数在执行完毕之后,会释放它所占用的内存,其中保存的变量相应的也会被释放,但是当我们在执行createClosure函数时,返回了一个匿名函数,在匿名函数内部用到了createClosure函数中的局部变量num,因此createClosure函数所占用的内存空间得不到释放(一旦释放将无法取得num变量)。第一次执行之前num的值为5,第一次执行之后num的值被修改为9并返回,第二次执行前num是第一次被修改后的值9,第二次执行后num被修改为13并返回(num的值会一直存在于内存中,直到calculate变量被清除)。

4.闭包的实现原理:闭包的实现原理与之前提到的作用域链密不可分,为了理解作用域链我们将其分为两个阶段来看:函数创建阶段和函数执行阶段。以上面的代码为例,在创建createClosure函数阶段,函数内部会自动创建一个[[Scope]]对象,这个对象我们可以理解为是一个栈,栈中依次存放的是指向createClosure函数外部作用域的指针(此例中外部只有全局作用域);接着在执行createClosure函数阶段,首先会创建一个活动对象,此活动对象包含了createClosure函数作用域内部的变量(this、参数、变量),然后在[[Scope]]对象的最前面压入一个指向活动对象的指针,在函数执行时,当需要访问变量值就会沿着[[Scope]]对象从前到后就近查找相应的变量值,此[[Scope]]对象看起来就行连接着一个个作用域的链条,因此叫做作用域链。
上面示例中createClosure函数返回了一个函数,并将自身作用域填入了返回函数的[[Scope]]对象中,我们将createClosure函数的返回值赋值给变量calculate,此时calculate就是对该函数的引用,只要此引用不被重新赋值,它的[[Scope]]对象就不会被释放,因此createClosure函数的作用域会一直存在。

5.闭包与变量:闭包只能获取包含函数中任何变量的最后一个值。

return array;
}

let result = createClosure();
console.log(result[0]()); // 10
console.log(result[1]()); // 10
console.log(result[2]()); // 10
console.log(result[8]()); // 10

实例中返回了一个数组,数组的每个元素都是一个匿名函数,因此当我们调用createClosure()函数时,实际上形成了十个闭包(每个元素都是一个闭包),闭包中使用了局部变量i,因此通过result[0]()调用函数时,可以访问到闭包中的局部变量i,但是由于在调用result[0]()时,i的值已经变为10了,因此不管调用哪个元素得到i的值都是10。
解决以上问题,有两种方法:1.使用ES6中的let关键字声明局部变量 2.返回一个立即执行的函数,并把i当做参数传入,在该函数中在返回一个匿名函数

函数
        return num;
      };
    }(i);
  }

return array;
}

let result = createClosure();
console.log(result[0]()); // 0
console.log(result[1]()); // 1
console.log(result[2]()); // 2
console.log(result[8]()); // 8

6.this对象:this对象较难理解的就是其指向问题,但是只要记住一点即分析出this的指向问题:谁调用指向谁!

getName();                                    // window
object1.getName(); // object1
(object1.getName = object1.getName)(); // window
console.log(object1.getName = object1.getName) // 赋值语句返回的值是函数本身function() {console.log(this.name);}
object2.getName()(); // window

第一次我们将object1.getName方法赋值给了一个全局变量getName,然后在全局中调用getName(),这就相当于是window对象调用了getName()方法,this指向window。
第二次直接使用object1.getName()调用方法,此时调用者是object1,因此this指向object1。
第三次我们执行了一个赋值操作,因为赋值语句的值是函数本省,因此也是相当于直接在全局调用了getName()方法,this指向window。
第四次执行object2.getName()函数得到的返回值是一个匿名函数,给匿名函数后面加上()就是调用函数,也是相当于在全局调用了该匿名函数,this指向window。

7.模仿块级作用域:在ES5及之前是没有块级作用域的概念的,只有全局作用域和函数作用域,ES6中提出了块级作用域的概念。不过我们也可以在ES5中通过函数作用域来模仿块级作用域:

console.log(i)                // 10
console.log(temp_i) // 9
console.log(j) // error,j is not defined
console.log(temp_j) // error,temp_j is not defined

上面示例中使用一个立即执行的函数来模拟块级作用域,在函数体内部就相当于是一个块级作用域,在块级作用域内声明的变量,在块级作用域外无法访问。

8.私有变量和特权方法:私有变量就是在外部无法访问的变量(函数作用域中的变量都是私有变量),特权方法就是可以访问私有变量的方法

函数
    count++;
  }
  this.getName = function() {     // 特权方法
    console.log(name);
  }
  this.changeCount = function() {  // 特权方法
    addCount();
    console.log(count);
  }
}

var obj1 = new object("dayday");
obj1.getName(); // dayday
obj1.changeCount(); // 1

示例中使用构造函数为每个object函数对象的实例都添加一个私有变量count,一个私有方法addCount,两个特权方法getName和changeCount。想要获取修改私有变量唯一的方法就是调用特权方法,除此之外没有任何方法可以访问私有变量。利用私有变量和特权方法可以隐藏那些不应该被直接修改的数据。

9.静态私有变量:上面的示例中通过构造函数的的方式为每个实例都创建了一份私有变量和特权方法,且每个实例中的私有变量和特权方法互不影响,没有联系。
我们还可以通过静态私有变量的方法来实现每个实例共享一个特权方法,且所有的特权方法共享私有变量(一个实例的私有变量被修改,其他实例相应的也会被修改)。

var people1 = new Person("dayday");
console.log(people1.getName()); // dayday
var people2 = new Person("huahua");
console.log(people1.getName()); // huahua
console.log(people2.getName()); // huahua
console.log(people1.getName == people2.getName); // true

people1.setName("mingming");
console.log(people1.getName()); // mingming
console.log(people2.getName()); // mingming

上面示例中通过构造函数Person创建了两个实例:people1和people2,通过控制台打印出来的信息来看,people1和people2共享私有变量name,同时由于将特权方法定义在原型上,因此实例也共享特权方法getName()和setName()。

相关文章

前言 做过web项目开发的人对layer弹层组件肯定不陌生,作为l...
前言 前端表单校验是过滤无效数据、假数据、有毒数据的第一步...
前言 图片上传是web项目常见的需求,我基于之前的博客的代码...
前言 导出Excel文件这个功能,通常都是在后端实现返回前端一...
前言 众所周知,js是单线程的,从上往下,从左往右依次执行,...
前言 项目开发中,我们可能会碰到这样的需求:select标签,禁...