1 函数 function

  • JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
  • 由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。

1.1 函数声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明

1.1.1 function 命令

function print(s) {
  console.log(s);
}

1.1.2 函数表达式

  1. 将一个匿名函数赋值给变量
var print = function (s) {
  console.log(s);
};
  1. 将具名匿名函数赋值给变量,该函数名只在函数体内部有效,在函数体外部无效
  • 一是可以在函数体内部调用自身,
  • 二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
var print = function x() {
  console.log(typeof x);
};

x; // ReferenceError: x is not defined

print(); // function

1.1.3 Function 构造函数

这种声明函数的方式非常不直观,几乎无人使用

var add = new Function("x", "y", "return x + y");

// 等同于
function add(x, y) {
  return x + y;
}

1.1.4 箭头函数

  • 箭头函数(=>):函数简写
    • 无参数:() => {}
    • 单个参数:x => {}
    • 多个参数:(x, y) => {}
    • 解构参数:({x, y}) => {}
    • 嵌套使用:部署管道机制
    • this 指向固定化
      • 并非因为内部有绑定 this 的机制,而是根本没有自己的 this,导致内部的 this 就是外层代码块的 this
      • 因为没有 this,因此不能用作构造函数

箭头函数误区

  • 函数体内的 this 是定义时所在的对象而不是使用时所在的对象

  • 可让 this 指向固定化,这种特性很有利于封装回调函数

  • 不可当作构造函数,因此箭头函数不可使用 new 命令

  • 不可使用 yield 命令,因此箭头函数不能用作 Generator 函数

  • 不可使用 Arguments 对象,此对象在函数体内不存在(可用 rest/spread 参数代替)

  • 返回对象时必须在对象外面加上括号

  • 箭头函数: () => {} 没有自己的 this 对象, 内部的 this 就是定义时上层作用域中的 this, this 指向是固定的

  • 普通函数: fun() {} 内部的 this 指向函数运行时所在的对象, this 指向是可变的

1.2 函数提升

  • JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
f();

function f() {}
  • 表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。

1.3 函数属性

  • 函数的name属性返回函数的名字。
  • 函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

1.4 函数方法

  • 函数的toString()方法返回一个字符串,内容是函数的源码。

1.5 函数作用域

作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:

  • 一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;

  • 一种是函数作用域,变量只在函数内部存在。在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。

  • ES6 又新增了块级作用域

  • 函数内部定义的变量,会在该作用域内覆盖同名全局变量。

  • 对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。

  • 与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

  • 函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

  • 很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。

1.6 函数参数

  • 函数参数不是必需的,JavaScript 允许省略参数,但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined

  • 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。

  • 如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

  • 如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值

    • 这是因为,形式参数(o)的值实际是参数obj的地址,重新对o赋值导致o指向另一个地址,保存在原地址上的值当然不受影响
var obj = [1, 2, 3];

function f(o) {
  o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]
  • 如果有同名的参数,则取最后出现的那个值
  • 调用函数f()的时候,没有提供第二个参数,a的取值就变成了undefined。这时,如果要获得第一个a的值,可以使用arguments对象。

1.7 函数尾调用

函数尾调用 :某个函数的最后一步是调用另一个函数

function f(x){
  return g(x);
}

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

以下三种情况,都不属于尾调用。

// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}

上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。

function f(x){
  g(x);
  return undefined;
}
  • 尾调用优化:只保留内层函数的调用帧
    • 尾调用
      • 定义:某个函数的最后一步是调用另一个函数
      • 形式:function f(x) { return g(x); }
    • 尾递归
      • 定义:函数尾调用自身
      • 作用:只要使用尾递归就不会发生栈溢出,相对节省内存
      • 实现:把所有用到的内部变量改写成函数的参数并使用参数默认值

1.8 arguments 对象

  • arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
  • 虽然arguments很像数组,但它是一个对象

1.8.1 arguments 属性

  • arguments.length属性,可以判断函数调用时到底带几个参数。
  • arguments.callee属性,返回它所对应的原函数。

1.9 闭包

  • 把闭包简单理解成 定义在一个函数内部的函数 ,能够读取其他函数内部变量的函数
  • 闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc(); // 5
inc(); // 6
inc(); // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

JS 中 return 一个函数与直接 return 一个函数变量的区别
函数的节流与防抖

function makeCounter() {
  var count = 0;
  function counter() {
    count = count + 1;
    return count;
  }
  return counter(); // 将嵌套函数返回
}
var doCount = makeCounter();
console.log(doCount, "--doCount1"); // 1 '--doCount1'
console.log(doCount, "--doCount2"); // 1 '--doCount2'
console.log(doCount, "--doCount3"); // 1 '--doCount3'
  • 当 return counter()时,就自动调用了嵌套函数。
  • 那么嵌套函数返回一个经过+1 的 count,并且 count 的值为 1.
  • 所以 doCount 得到的是一个数字,并不是函数,所以无法得到闭包。
function makeCounter() {
  var count = 0;
  function counter() {
    count = count + 1;
    return count;
  }
  return counter; // 将嵌套函数返回,但只写函数名称
}
var doCount = makeCounter();
console.log(doCount(), "--doCount1"); // 1 '--doCount1'
console.log(doCount(), "--doCount2"); // 2 '--doCount2'
console.log(doCount(), "--doCount3"); // 3 '--doCount3'
  • return counter 返回的是整一个 cunter()函数。

  • 因此执行 var doCount = makeCounter()时,doCount 将引用 counter 函数及其中的变量环境。

  • 那么 counter 函数及其中的变量环境,就是闭包了

  • 闭包的形成:内部函数引用了外部函数的数据(这里为 count),

  • 因此在 doCount=makeCounter(),则会把这个 count 保存在 doCount 中,函数执行完,count 并不会被销毁。

  • 注意: 为什么上面这段代码没有直接写的 function doCount(){…} 而是把 function 赋值给了变量 doCount 呢?

  • 我们通常会想当然的认为每次调用 doCount() 都会重走一遍 doCount()中的代码块, 但其实不然。

  • 注意 makeCounter 方法中的 return 不是 1,2,3 这样的数值, 而是一个方法,并且把这个方法赋值给了 doCount 变量。

  • 那么在这个 makeCounter 自运行一遍之后, 其实最后赋值给 doCount 的是 count = count + 1; return count; 这段代码。

  • 所以后面每次调用 doCount() 其实都是在调用 count = count + 1; return count;

  • 闭包会持有父方法的局部变量并且不会随父方法销毁而销毁, 所以这个 count 其实就是来自于第一次 makeCounter 执行时创建的变量

1.10 立即调用的函数表达式(IIFE)

  • 根据 JavaScript 的语法,圆括号()跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

1.11 eval 命令

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

eval("var a = 1;");
a; // 1

使用严格模式 加一行 ‘use strict’;

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐