函数调用

  1. /* 1. 普通函数 */
  2. function fn() {
  3. console.log('人生的巅峰');
  4. }
  5. fn();
  6. /* 2. 对象的方法 */
  7. var o = {
  8. sayHi: function() {
  9. console.log('人生的巅峰');
  10. }
  11. }
  12. o.sayHi();
  13. /* 3. 构造函数*/
  14. function Star() {};
  15. new Star();
  16. /* 4. 绑定事件函数*/
  17. btn.onclick = function() {}; // 点击了按钮就可以调用这个函数
  18. /* 5. 定时器函数*/
  19. setInterval(function() {}, 1000); 这个函数是定时器自动1秒钟调用一次
  20. /* 6. 立即执行函数(自调用函数)*/
  21. (function() {
  22. console.log('人生的巅峰');
  23. })();

2.1 函数方法

apply()和call()这两个方法以指定的this值来调用函数,设置指定函数内部this指向的值

apply()方法

  1. - apply()方法接受两个参数:函数内部this的值、一个参数组成的数组(可以是Arrat的实例、也可以是arguments对象)
  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. function callSum1(num1, num2) {
  5. return sum.apply(this, arguments); // 传入 arguments 对象
  6. }
  7. function callSum2(num1, num2) {
  8. return sum.apply(this, [num1, num2]); // 传入数组
  9. }
  10. console.log(callSum1(10, 10)); // 20
  11. console.log(callSum2(10, 10)); // 20

在这个例子中,callSum1()会调用 sum()函数,将 this 作为函数体内的 this 值(这里等于 window,因为是在全局作用域中调用的)传入,同时还传入了 arguments 对象。callSum2()也会调 用 sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。

注意!在严格模式下,调用函数没有指定上下文对象,则this值不会指向window,除非使用apply()或call()把函数指定给一个对象,否则this的值会变成undefined。

call()方法

  1. - call()方法传参形式不同:第一个和apply()一样,也是this值,而剩下的要传给被调用函数的参数则是个传递的。换句话说,通过 call()向函数传参时,必须将参数一个一个地列出来
  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. function callSum(num1, num2) {
  5. return sum.call(this, num1, num2);
  6. }
  7. console.log(callSum(10, 10)); // 20
  • 这里的 callSum()函数必须逐个地把参数传给 call()方法。
  • 结果跟 apply()的例子一样。
  • 到底是使用 apply()还是 call(),完全取决于怎么给要调用的函数传参更方便。
  • 如果想直接传 arguments对象或者一个数组,那就用 apply();否则,就用 call()。
  • 当然,如果不用给被调用的函数传参,则使用哪个方法都一样。

bind()方法

ECMAScript 5 出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind()的对象。

  1. window.color = 'red';
  2. var o = {
  3. color: 'blue'
  4. };
  5. function sayColor() {
  6. console.log(this.color);
  7. }
  8. let objectSayColor = sayColor.bind(o);
  9. objectSayColor(); // blue

2.2.4 call、apply、bind三者的异同

共同点 : 都可以改变this指向

  • 不同点:
    • call 和 apply 会调用函数, 并且改变函数内部this指向.
    • call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递
    • bind 不会调用函数, 可以改变函数内部this指向.
  • 应用场景
    1. call 经常做继承.
    2. apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
    3. bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.

2.2 函数表达式

定义函数有两种方式:

  • 函数声明
  • 函数表达式

函数声明

  1. function functionName(arg0, arg1, arg2) {
  2. // 函数体
  3. }

函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后:

  1. sayHi();
  2. function sayHi() {
  3. console.log("Hi!");
  4. }

这个例子不会抛出错误,因为 JavaScript 引擎会先读取函数声明,然后再执行代码。


函数表达式

函数表达式有几种不同的形式,最常见的是这样的:

  1. let functionName = function(arg0, arg1, arg2) {
  2. // 函数体
  3. };
  • 函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。
  • 这样创建的函数叫作匿名函数(anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。
  • 未赋值给其他变量的匿名函数的 name 属性是空字符串。

函数表达式跟 JavaScript 中的其他表达式一样,需要先赋值再使用。下面的例子会导致错误:

  1. sayHi(); // Error! function doesn't exist yet
  2. let sayHi = function() {
  3. console.log("Hi!");
  4. };

理解函数声明与函数表达式之间的区别,关键是理解提升。比如,以下代码的执行结果可能会出乎意料:

  1. // 千万别这样做!!!!!
  2. if (condition) {
  3. function sayHi() {
  4. console.log('Hi!');
  5. }
  6. } else {
  7. function sayHi() {
  8. console.log('Yo!');
  9. }
  10. }

这段代码看起来很正常,就是如果 condition 为 true,则使用第一个 sayHi()定义;否则,就使用第二个。事实上,这种写法在 ECAMScript 中不是有效的语法。JavaScript 引擎会尝试将其纠正为适当的声明。问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略 condition 直接返回第二个声明。Firefox 会在 condition 为 true 时返回第一个声明。这种写法很危险,不要使用。不过,如果把上面的函数声明换成函数表达式就没问题了

  1. // 没问题
  2. let sayHi;
  3. if (condition) {
  4. sayHi = function() {
  5. console.log("Hi!");
  6. };
  7. } else {
  8. sayHi = function() {
  9. console.log("Yo!");
  10. };
  11. }

2.3 递归

递归函数通常的形式是一个函数通过名称调用自己

  1. function fn(num) {
  2. if (num <= 1) { 如果乘到1就不再递归了
  3. return 1;
  4. } else {
  5. return num * fn(num - 1);
  6. }
  7. }

这是一个简单的递归阶乘函数,但是将这个递归函数赋值给其他变量使用时,会出现问题

  1. let newFunction = fn;
  2. fn=null; //将之前的递归阶乘函数设置为null
  3. console.log(newFunciton(4));//报错

由于复制后fn被设置为空,再执行赋值后的函数时,里面递归函数名字仍然时fn(),这时fn已经为null了无法执行直接报错。
在写递归函数时使用 arguments.callee可以避免这个问题。
arguments.callee 就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,如下所示:

  1. function factorial(num) {
  2. if (num <= 1) {
  3. return 1;
  4. } else {
  5. return num * arguments.callee(num - 1); //将函数名替换成了 arguments.callee
  6. }
  7. }

把函数名称替换成 arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee 是引用当前函数的首选。不过,在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的

  1. const factorial = (function f(num) {
  2. if (num <= 1) {
  3. return 1;
  4. } else {
  5. return num * f(num - 1);
  6. }
  7. });

这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。


2.4 闭包

匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数通常是在嵌套函数中实现的。闭包(closure)指有权访问另一个函数作用域中变量的函数。简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。

  1. function fn1(){ //fn1包含着fn2 fn1为闭包函数
  2. let num=10;
  3. funciton fn2(){
  4. console.log(num); //10
  5. }
  6. fn2() //掉用fn2函数
  7. }
  8. fn1() //调用fn1函数

闭包延伸了变量的作用范围

  1. function fn() {
  2. var num = 10;
  3. function fun() {
  4. console.log(num);
  5. }
  6. return fun;
  7. }
  8. var f = fn();
  9. f(); //10

利用闭包获得当前li的索引

  1. for (var i = 0; i < lis.length; i++) {
  2. // 利用for循环创建了4个立即执行函数
  3. // 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
  4. (function(i) {
  5. lis[i].onclick = function() {
  6. console.log(i);
  7. }
  8. })(i);
  9. }