# 函数的深入浅出

JavaScript 函数是被设计为执行特定任务的代码块。

JavaScript 函数会在某代码调用它时被执行。

通俗来讲:函数就是把多行代码包起来成代码块,可以通过调用函数来重复使用代码块。(也就是把重复的代码封装起来,通过调用函数执行代码块)

# JavaScript 函数语法

JavaScript 函数通过 function 关键词进行定义,其后是函数名和括号 ()和花括号{}。

function 函数名(){} 

函数名可包含字母数字下划线美元符号规则与变量名相同)。

圆括号可包括由逗号分隔的参数:

(参数 1, 参数 2, ...)

由函数执行的代码被放置在花括号中:{}

function (参数 1,funName 参数 2, 参数 3) {
    要执行的代码
}

在函数中,参数是局部变量。

# 函数声明

常用定义一个函数的方式有两种

function funName() {};

var funName = function () {};

这里给大家讲讲函数声明提升,和变量的提升

函数声明提升与变量提升

JS在编译阶段,函数声明和变量声明都会被先处理置于执行环境的顶部,且赋值会被留在原地,这个过程称之为提升。

举个简单例子:

console.log(fn);
console.log(i);
var i = 1;
function fn () {
  console.log(2)
}

实际上代码顺序是这样的:

 var fn = function () {
  console.log(2)
}
var i;
console.log(fn);
console.log(i);
i = 1;

# 变量提升

变量声明在编译阶段被处理,而变量赋值则留在原地等待执行。

console.log(i);   // undefined
var i = 1;
console.log(i);   // 1

相当于:

var i;
console.log(i);   // 由于i只是声明未赋值,输出undefined
i = 1;
console.log(i)    // i已赋值,输出1

一道测试题

 var age = 10;
  function person () {
      age = 100;
      console.log(age);  // 100
      var age;
      console.log(age)  // 100
  }
  person();
  console.log(age);   // 10

这里可以在页面上设置断点调试看看 局部作用域的age 和 全局作用域的age

# 函数提升

js解析器在解析时对函数声明与函数表达式有着不同的优先级,实际上编译阶段函数声明会先于变量被提升,并使其在执行任何代码之前可访问,函数表达式实际上是变量声明的一种,因此函数声明提升优于函数表达式

console.log(fn(1));    // 1
function fn (a) {
    return a;
}

如上面的代码,由于函数声明被置于执行环境顶部,即使调用函数的代码在声明函数之前也可以正确访问。再看函数表达式的例子:

console.log(fn(1));
var fn = function (a) {
    return a;
}
// fn is not a function

相当于

var fn;
console.log(fn(1));
fn = function (a) {
  return a;
}
// fn is not a function

上面的例子之所以报错,是因为变量fn声明后还未对函数引用(fn还没赋值)。

另外函数声明提升不会被变量声明覆盖,但会被变量赋值覆盖。

变量未赋值的例子:

function fn() {
	console.log(1);
}
var fn;
console.log(fn);    // 由于后一个fn只声明未负值,因此输出的是函数fn

变量赋值的例子:

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

相当于

function fn(){
	console.log(1);
}
var fn;
fn = function () {
	console.log(2)
};
fn();    // 2(因为声明的函数fn被后一个已引用函数的变量fn所覆盖,因此输出2)

再来点例子

fn();
var fn = function () {
	console.log(1);
}
fn();
function fn () {
	console.log(2);
}
var fn;
fn();
// 依次输出2,1,1

# 局部 JavaScript 变量

在 JavaScript 函数内部声明的变量(使用 var)是局部变量,所以只能在函数内部访问它。(该变量的作用域是局部的)。

您可以在不同的函数中使用名称相同的局部变量,因为只有声明过该变量的函数才能识别出该变量。

只要函数运行完毕,本地变量就会被删除。

# 全局 JavaScript 变量

在函数外声明的变量是全局变量,网页上的所有脚本和函数都能访问它。

# 变量的作用域

变量在哪里声明,则那里就是它的有效区域

var scope = "global";		//定义全局变量
function checkscope(){		
	var scope = "local";		//定义局部变量
	console.log(scope);			
}
checkscope();				          //=>'local'		改变的只是局部变量scope
console.log(scope);						//=>'global'		全局变量scope并没有改变

在函数体内的参数使用var定义的变量都是属于局部变量,一定要善于使用局部变量,防止污染全局变量

# 变量的声明提前

用了var定义的变量或函数的参数,都会被提前到函数体的顶部进行声明(但不涉及赋值,具体赋值还是需要运行到对应行数)

var scope = "global";
function f() {
	console.log(scope); // =>undefined 			输出"undefined",而不是"global" 
	var scope = "local"; // 变量 在这里 赋 初始 值, 但 变量 本身 在函数体内任何地方均是有定义的
	console.log(scope);	 // =>"local"
}

这个特性跟全局变量的声明提前其实是一致的,所以编程时,要有个好习惯,尽量把函数所需要用到的变量在函数顶部声明好,这样可以使源代码非常真实的反映函数的变量作用域

# JavaScript 变量的生存期

JavaScript 变量的生命期从它们被声明的时间开始。

局部变量会在函数运行以后被删除。

全局变量会在页面关闭后被删除。

向未声明的 JavaScript 变量分配值

如果您把值赋给尚未声明的变量,该变量将被自动作为 window 的一个属性。

这条语句:

carname="Volvo";

将声明 window 的一个属性 carname。

非严格模式下给未声明变量赋值创建的全局变量,是全局对象的可配置属性,可以删除。

var a = 1;
function fn () {
    b = 2;
}
fn();
console.log(a);
console.log(b);
console.log(this.a);
console.log(this.b);
console.log(this === window);
console.log(delete a); // false 无法删除
console.log(a); //1
console.log(delete b); // false 无法删除
console.log(b); //1

# 函数调用

函数可以通过其名字加上括号进行调用,有参数的就括号里面写参数,多个参数就英文逗号隔开

function fn(a, b){
  console.log(a);
  console.log(b);
}

//通过函数名加()调用函数
fn();
fn(1);
fn(1, 2);

# 为何使用函数?

您能够对代码进行复用:只要定义一次代码,就可以多次使用它。

您能够多次向同一函数传递不同的参数,以产生不同的结果。

function fn (a) {
  var num = 0;
  for (var i = 0; i < a; i ++) {
    num += i;
  }
  console.log(num);
  return num;
}
fn(10);

细心的同学就看到我上面的例子函数里面,写了个return num; 这个return是什么呢?

# return 语句

当 JavaScript 到达 return 语句,函数将停止执行。(函数体内没有写return的时候,默认在函数体最后有个return;)

函数通常会计算出返回值。这个返回值会返回给调用者

return 语句 只能 在 函数 体内 出现, 如果不 是的 话 会 报 语法 错误。 当 执行 到 return 语句 的 时候, 函数 终止 执行, 并 返回 expression 的 值 给 调用 程序。

看下面例子 ,计算两个数的乘积,并返回结果:

var x = myFunction(7, 8);        // 调用函数,返回值被赋值给 x

console.log(x);

function myFunction(a, b) {
    return a * b;                // 函数返回 a 和 b 的乘积
}

# JavaScript 函数参数

javascript函数的参数与大多数其他语言的函数的参数有所不同。函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型,甚至可以不传参数。

Javascript的参数分为实参形参两种类型:

实参:从字面意义我们可以理解为“实际存在的参数”,是在函数调用时传给函数的变量,该变量在函数执行时必须存在。实参可以为变量、常量、函数、表达式等。

形参:从字面意义我们可以理解为“形式上存在的参数”,由此我们可以看出它并不是真实存在的参数,又称为虚拟变量。它在函数定义时使用,作用为接收函数调用时的实参。

在JavaScript中实参与形参数量并不需要像JAVA一样必须在数量上严格保持一致,具有很大的灵活性。如下:

function test(str1, str2, str3) {
    console.log(str1);
    console.log(str2);
    console.log(str3);
}
test();                             // str1: undefined, str2: undefined, str3: undefined
test('hello');                      // str1: 'hello', str2: undefined, str3: undefined
test('hello', 'world');             // str1: 'hello', str2: 'world', str3: undefined
test('hello', 'world', '!');        // str1: 'hello', str2: 'world', str3: '!'

在JavaScript代码运行过程中,形参的作用为接收实参,它们两个分别位于不同的内存地址中,大致可以分为两种情况:

  1. 实参原始值。当实参为原始值时,此时形参实参拷贝。因此,函数体内形参值的改变并不会影响实参
function test(str) {
    str = 'chinese';
    return str;
}
const str1 = 'china';
const str2 = test(str1);
console.log(str1);      // china
console.log(str2);      // chinese
  1. 实参引用值。当实参为引用值时,此时形参为实参内存地址的拷贝。因此,函数体内形参值的变化一定情况下将会影响实参
function test(obj) {
    // 形参obj的值实际上为实参obj的内存引用,及形参与实参同时指向同一个内存地址。
    obj.name = 'typeScript';    // 此时改变的为形参与实参同时指向的那个内存地址中的值
                                // 所以此时也导致实参的name属性发生了变化
    obj = {                     // 此时对形参obj进行重新赋值,给予了它一个新的内存地址
        name: 'react',          // 从此之后的形参将于实参完全解绑,两者之前不再存在联系
        star: 13000,
    }
    obj.star = 20000;           // 所以这里仅仅是改变了形参的star属性
    return obj;
}
const obj1 = {
    name: 'javaScript',
    star: 100000,
}
const obj2 = test(obj1);
console.log(obj1);      // name: 'typeScript', star: 100000
console.log(obj2);      // name: 'react', star: 20000

同名形参

在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。

function add(x,x,x){
  return x;
}
console.log(add(1,2,3)); //3

而在严格模式下,出现同名形参会抛出语法错误

function add(x,x,x){
'use strict';
return x;
}
console.log(add(1,2,3));//SyntaxError: Duplicate parameter name not allowed in this context

参数个数

当实参比函数声明指定的形参个数要少,剩下的形参都将设置为undefined值

function add(x, y){
  console.log(x, y); //1 undefined
}
add(1);

常常使用逻辑或运算符给省略的参数设置一个合理的默认值

function add(x, y){
  y = y || 2;
  console.log(x, y); //1 2
}
add(1);

更多设置默认参数的方法:ES5和ES6中对函数设置默认参数的方法总结

当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用即将提到的arguments对象

# arguments对象

javascript中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数。arguments对象并不是Array的实例,它是一个类数组对象,可以使用方括号语法访问它的每一个元素

function add(x){
  console.log(arguments[0], arguments[1], arguments[2]) //1 2 3
  return x + 1;
}
console.log(add(1,2,3)); //2

arguments对象的length属性显示实参的个数,函数的length属性显示形参的个数

function add(x, y){
  console.log(arguments.length); //3
  return x + 1;
}
add(1, 2, 3);
console.log(add.length); //2

形参只是提供便利,但不是必需的

function add(){
  return arguments[0] + arguments[1];
}
console.log(add(1, 2)); //3

当形参与实参的个数相同时,arguments对象的值和对应形参的值保持同步

function test(num1, num2){
  console.log(num1, arguments[0]); //1 1
  arguments[0] = 2;
  console.log(num1, arguments[0]); //2 2
  num1 = 10;
  console.log(num1, arguments[0]); //10 10
}
test(1);

[注意]虽然命名参数和对应arguments对象的值相同,但并不是说读取两个值会访问相同的内存空间。它们的内存空间是独立的,但值是同步的

但在严格模式下,arguments对象的值和形参的值是独立的

function test(num1, num2){
  'use strict';
  console.log(num1, arguments[0]); //1 1
  arguments[0] = 2;
  console.log(num1, arguments[0]); //1 2
  num1 = 10;
  console.log(num1, arguments[0]); //10 2
}
test(1);

当形参并没有对应的实参时,arguments对象的值与形参的值并不对应

function test(num1,num2){
  console.log(num1, arguments[0]); //undefined,undefined
  num1 = 10;
  arguments[0] = 5;
  console.log(num1, arguments[0]); //10,5
}
test(); 

arguments对象的属性callee

arguments对象有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数

下面是经典的阶乘函数

function factorial(num){
  if(num <=1){
    return 1;
  }else{
    return num* factorial(num-1);
  }
} 
console.log(factorial(5)); //120

但是,上面这个函数的执行与函数名紧紧耦合在了一起,可以使用arguments.callee可以消除函数解耦

function factorial(num){
  if(num <= 1){
    return 1;
  }else{
    return num * arguments.callee(num - 1);
  }
} 
console.log(factorial(5)); //120

但在严格模式下,访问这个属性会抛出TypeError错误

function factorial(num){
  'use strict';
  if(num <=1){
   return 1;
  }else{
    return num * arguments.callee(num - 1);
  }
} 
//TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
console.log(factorial(5));

# 对象参数

当一个函数包含超过3个形参时,要记住调用函数中实参的正确顺序实在让人头疼

通过键/值对的形式来传入参数,这样参数的顺序就无关紧要了。定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的键/值对是真正需要的实参数据

function fn(args){
  args.a && console.log(args.a);
  args.b && console.log(args.b);
  args.c && console.log(args.c);
}
fn({
  a: 1,
  b: [1,2]
});

# 函数重载

javascript函数不能像传统意义上那样实现重载。而在其他语言中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可

javascript函数没有签名,因为其参数是由包含0或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的

//后面的声明覆盖了前面的声明
function addSomeNumber(num){
  return num + 100;
}
function addSomeNumber(num){
  return num + 200;
}
var result = addSomeNumber(100); //300

只能通过检查传入函数中参数的类型和数量并作出不同的反应,来模仿方法的重载

function doAdd(){
  if(arguments.length == 1){
    alert(arguments[0] + 10);
  }else if(arguments.length == 2){
    alert(arguments[0] + arguments[1]);
  }
}
doAdd(10); //20
doAdd(30, 20); //50

# es6函数的扩展

# 函数参数的默认值

在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

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

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

上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

if (typeof y === 'undefined') {
  y = 'World';
}

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

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

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

可以看到,ES6的写法比ES5简洁许多,而且非常自然。下面是另一个例子。

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

var p = new Point();
p // { x: 0, y: 0 }

除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

参数变量是默认声明的,所以不能用let或const再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

上面代码中,参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。

# 与解构赋值默认值结合使用

参数默认值可以与解构赋值的默认值,结合起来使用。

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

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined

上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效。

下面是另一个对象的解构赋值默认值的例子。

function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 报错

上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。

上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

function fetch(url, { method = 'GET' } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。

# 参数默认值的位置

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

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]

上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。

# 函数的length属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

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

上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了3个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2。

这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。

(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

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

# 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

上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。

rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)
console.log(a);

注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}

函数的length属性,不包括rest参数。

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

# 匿名函数

没有函数名的函数,叫做匿名函数

setTimeout(function(){
    console.log('我是匿名函数');
}, 1000);

上面例子中的 function(){}, 就叫做匿名函数

# 回调函数

setTimeout(function(){
    console.log('我是匿名函数');
}, 1000);

上面例子中的 function(){}, 就叫做匿名函数, 也可以叫回调函数,就是一个函数当作参数传入,给别人函数内来调用,也就是回调函数

# 立即执行函数

声明了函数后加括号,就立即执行

(function(){ console.log('我是立即执行函数') })();

# 递归函数

自己调用自己,就是递归函数了

let num = 0;
function add(){
    console.log(num++);
    add();
}

# 箭头函数

let fn = () => { console.log('我是箭头函数') }
fn();
let fn1 = res => console.log(res);
fn1(1)
更新时间: 2020年11月29日星期日晚上7点39分