Function知识点整理-JavaScript

200 阅读11分钟

Function知识点整理

Function知识点整理.png

Function思维导图

1. 函数概述

1.1 函数声明

function 命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

function print(s) {
  console.log(s);
}
函数表达式

除了用function命令声明函数,还可以采用变量赋值的写法。将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

var print = function(s) {
  console.log(s);
};
Function 构造函数

第三种声明函数的方式是Function构造函数。最后一个参数是函数体,前面的都是函数的参数

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

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

1.2 函数的其他特性

函数的重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。由于函数名的提升,前一次声明在任何时候都是无效的

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2
第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。

function add(x, y) {
  return x + y;
}
// 将函数赋值给一个变量
var operator = add;
// 将函数作为参数和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2
函数名的提升

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

但是,如果采用赋值语句定义函数,JavaScript 就会报错。采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。

f();
function f() {}

f();
var f = function (){};
// TypeError: undefined is not a function

var f = function () {
  console.log('1');
}
function f() {
  console.log('2');
}
f() // 1

2. 函数的属性和方法

2.1 函数的属性

name 属性

函数的name属性返回函数的名字。如果是通过变量赋值定义的函数,那么name属性返回变量名。

function f1() {}
f1.name // "f1"
var f2 = function () {};
f2.name // "f2"
length 属性

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

function f(a, b) {}
f.length // 2

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

2.2 函数的方法

toString()

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

function f() {
  a();
  b();
  c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }

3. 函数作用域

3.1 函数作用域的定义

JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量

3.2 函数内部的变量提升

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

function foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}
// 等同于
function foo(x) {
  var tmp;
  if (x > 100) {
    tmp = x - 100;
  };
}

3.3 函数本身的作用域

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

var a = 1;
var x = function () {
  console.log(a);
};
function f() {
  var a = 2;
  x();
}
f() // 1

4. 函数参数

参数省略

函数参数不是必需的,JavaScript 允许省略靠后的参数。

function f(a, b) {
  return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2

参数传递

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

var p = 2;
function f(p) {
  p = 3;
}
f(p);
p // 2

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

var obj = { p: 1 };
function f(o) {
  o.p = 2;
}
f(obj);
obj.p // 2

同名参数

如果有同名的参数,则取最后出现的那个值。

function f(a, a) {
  console.log(a);
}
f(1, 2) // 2

arguments 对象

arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。修改arguments对象不会影响到实际的函数参数。通过arguments对象的length属性,可以判断函数调用时到底带几个参数。

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3

参数默认值

ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

参数变量是默认声明的,所以不能用letconst再次声明。使用参数默认值时,函数不能有同名参数。

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

// es5写法
function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

// es6写法
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

// 参数默认值位置
function f(x = 1, y) {
  return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}
foo() // 100
x = 100;
foo() // 101

参数默认值解构赋值

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

参数默认值作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2

上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x

rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

rest 参数代替arguments变量的例子。

// arguments变量的写法
function sortNumbers() {
  return Array.from(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

5. 函数高阶

5.1 闭包

把闭包简单理解成“定义在一个函数内部的函数”。闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7

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

函数定义后立即调用的解决方法,就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。函数当作表达式时,函数可以定义后直接加圆括号调用。

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

var f = function f(){ return 1}();
f // 1

5.3. 箭头函数

ES6 允许使用“箭头”(=>)定义函数。

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var f = v => v;

// 等同于
var f = function (v) {
  return v;
};

箭头函数有几个使用注意点。

(1)箭头函数没有自己的this对象,箭头函数内部的this就是定义时上层作用域中的this。普通函数来说,内部的this指向函数运行时所在的对象。箭头函数内部的this指向是固定的 ,普通函数的this指向是可变的。

(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

5.4 函数柯里化

柯里化(currying),意思是将多参数的函数转换成单参数的形式

参考来源:juejin.cn/post/686437…

参数定长的柯里化

假设存在一个原函数fnfn接受三个参数a, b, c,那么函数fn最多被柯里化三次(有效地绑定参数算一次)。

function fn(a, b, c) {
  return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); // 6
// 再次柯里化也没有意义,原函数只需要三个参数
var c4 = curry(c3, 4);
c4();

也就是说,我们可以通过柯里化缓存的参数数量,来判断是否到达了执行时机。那么我们就得到了一个柯里化的通用模式。

function curry(fn) {
  // 获取原函数的参数长度
  const argLen = fn.length;
  // 保存预置参数
  const presetArgs = [].slice.call(arguments, 1)
  // 返回一个新函数
  return function() {
    // 新函数调用时会继续传参
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    if (allArgs.length >= argLen) {
      // 如果参数够了,就执行原函数
      return fn.apply(this, allArgs)
    } else {
      // 否则继续柯里化
      return curry.call(null, fn, ...allArgs)
    }
  }
}

这样一来,我们的写法就可以支持以下形式。

function fn(a, b, c) {
  return a + b + c;
}
var curried = curry(fn);
curried(1, 2, 3); // 6
curried(1, 2)(3); // 6
curried(1)(2, 3); // 6
curried(1)(2)(3); // 6
curried(7)(8)(9); // 24
参数不定长的柯里化

要支持参数不定长的场景,已经柯里化的函数在执行完毕时不能返回一个值,只能返回一个函数;同时要让JS引擎在解析得到的这个结果时,能求出我们预期的值。通过重写toString,就可以巧妙地实现我们的需求了。

function curry(fn) {
  // 保存预置参数
  const presetArgs = [].slice.call(arguments, 1)
  // 返回一个新函数
  function curried () {
    // 新函数调用时会继续传参
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    return curry.call(null, fn, ...allArgs)
  }
  // 重写toString
  curried.toString = function() {
    return fn.apply(null, presetArgs)
  }
  return curried;
}
复制代码

这样一来,魔性的add用法就都被支持了。

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) => {
    return prev + curr
  }, 0)
}
var add = curry(dynamicAdd);
add(1)(2)(3)(4) // 10
add(1, 2)(3, 4)(5, 6) // 21