1.7JavaScript语言的函数(Function)

1.Function()函数方式

Function() 函数方式创建的函数定义存在安全性和性能问题,所以不推荐使用,不过此种创建函数定义的方式很好地诠释了函数声明、函数表达式、箭头函数表达式是 Function 类型的实例这个概念。

//构造函数方式
{ let | const } 函数名 = new Function("形参1", "形参2", "形参N", "函数体");

//函数方式
{ let | const } 函数名 = Function("形参1", "形参2", "形参N", "函数体");
const sum = new Function("num1", "num2", "return num1 + num2");

const sum = Function("num1", "num2", "return num1 + num2");

//近似的函数声明方式
function sum(num1, num2) {
  return num1 + num2;
}

2.函数声明

注意:在同一个作用域,相同名称的函数可以重复声明,但后定义的会覆盖先定义的。

function 函数名(值形参名, 默认值形参名 = 默认值, ...剩余形参名) {
  //函数体
}

3.函数表达式

注意:在同一个作用域,相同名称的函数表达式不可以重复定义,否则会报错。

注意:命名函数表达式的右侧依然是函数表达式,不是函数声明。

//命名函数表达式
{ let | const } f = function f2(值形参名, 默认值形参名 = 默认值, ...剩余形参名) {
  //函数体
};

//匿名函数表达式
{ let | const } f = function (值形参名, 默认值形参名 = 默认值, ...剩余形参名) {
  //函数体
};

4.箭头(Arrow)函数表达式

注意:在同一个作用域,相同名称的箭头函数表达式不可以重复定义,否则会报错。

注意:箭头函数没有 prototype 属性,不可以直接用作构造函数,也不可以直接使用 argumentssupernew.target

没有形参或多个形参时必须使用圆括号 () ,只有一个值形参时可以省略圆括号 (),只有一个默认值形参或者只有一个剩余形参时不可以省略圆括号 ()

如果有花括号 {},则花括号 {} 内就跟普通函数一样。

如果没有花括号 {},则胖箭头 => 后面就只可以为单个表达式。因为 return 关键字只可以被用在函数体内,此时不可以使用 return 关键字。而且,箭头函数会隐式地将此单个表达式的值返回给此箭头函数的调用者。 另外,如果返回值为一个对象字面量,则需要将此对象字面量放在一对圆括号 () 中,以避免解释器分不清花括号到底是函数体的花括号还是对象字面量的花括号。

{ let | const } 函数名 = (值形参名, 默认值形参名 = 默认值, ...剩余形参名) => { 
  //函数体
};
let person = {};
let setPerson = (p) => p.name = "张三";
setPerson(person);
console.log(person);  // { name: "张三" }
//返回值为一个对象字面量
const f = x => { return { a: x }; };  // 语法正确

const f = x => ({ a: x });  // 语法正确

const f = x => { a: x };  // 对象字面量内的代码会被误认为是label语句,最终返回undefined。

const f = x => { a: x, b: x };  // 报错

5.立即调用的函数表达式

注意:立即调用的函数表达式(Immediately Invoked Function Expression,IIFE)主要用于在 JavaScript 语言支持块作用域语法之前模拟块作用域,目前已很少使用。

注意:在同一个作用域,相同名称的立即调用的函数表达式可以重复定义,相当于两个互不干扰的函数调用。

首先看一下函数调用表达式的语法:

//无实参
函数名();

//有实参
函数名(实参);

因为函数名是存储函数定义的变量或常量,所以将函数名直接替换为函数定义也是合法的。

在第一个圆括号内是函数的定义,紧跟在后面的第二个圆括号会立即调用前面第一组圆括号内的函数定义。

//函数表达式方式
//命名函数表达式
(function 函数名(形参) {
  //函数体
})(实参);
//匿名函数表达式
(function(形参) {
  //函数体
})(实参);

//箭头函数表达式形式
((形参) => {
  //函数体
})(实参);

6.函数声明提升(Hoisting)

注意:函数声明支持提升,函数表达式、箭头函数表达式不支持提升。

在任何代码运行之前,JavaScript 引擎会先运行一遍扫描,把发现的函数声明提升到此函数所在作用域的顶部,因此即使函数声明出现在函数调用之后,也可以正常运行。

而函数表达式、箭头函数表达式必须等到运行到它那一行才会发现它们,因此如果函数表达式、箭头函数表达式出现在函数调用之后,会报错。

//函数声明
console.log(sum(10, 10));  // 20
function sum(num1, num2) {
  return num1 + num2;
}
//函数表达式
//命名函数表达式
console.log(sum(10, 10));  // 报错
let sum = function sum2(num1, num2) {
  return num1 + num2;
};

//匿名函数表达式
console.log(sum(10, 10));  // 报错
let sum = function (num1, num2) {
  return num1 + num2;
};
//匿名函数表达式
console.log(sum(10, 10));  // 报错
let sum = (num1, num2) => {
  return num1 + num2;
};

7.函数作用域

函数声明、函数表达式、箭头函数表达式的本质都是将函数对象赋给变量或常量,因此,与变量和常量一样,函数的作用域也是块作用域,即外围离函数最近的代码块。全局函数的作用域是定义它们的文件。

7.1全局函数

名称修饰符
全局函数参考函数声明、函数表达式、箭头函数表达式。

7.2成员方法

名称修饰符
静态访问器属性(类)static getstatic set
静态方法(类)static
静态初始化块(类)static
实例访问器属性(类)getset
实例方法(类)——————
构造函数(类)——————
访问器属性(对象字面量)getset
方法(对象字面量)——————

7.3局部函数

名称修饰符
嵌套函数参考函数声明、函数表达式、箭头函数表达式。

8.函数名

函数名是第一次跟函数定义绑定的函数名或变量或常量或属性名。

8.1函数声明的函数名

function f() {}
console.log(f.name);                    // f
console.log( (function f() {}).name );  // f

函数声明是 Function 类型的实例,函数名就是存储这个实例的变量。

注意:函数声明的函数名是变量,不是常量,因为在赋值后可以通过重新赋值改变其值。

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

f = 2;
f();             // 报错
console.log(f);  // 2

8.2函数表达式的函数名

//命名函数表达式
const f = function f2() {};
console.log(f.name);                     // f2
console.log( (function f2() {}).name );  // f2

//匿名函数表达式
const f = function () {};
console.log(f.name);                   // f
console.log( (function () {}).name );  // ""(空字符串)

注意:命名函数表达式的函数名 f2 的作用域仅仅为函数体,比如可用于函数递归,但不可以在函数体外使用。

const f = function f2() {
  console.log(1);
};

f();   // 1
f2();  // 报错

8.3箭头函数表达式的函数名

//箭头函数表达式
const f = () => {};
console.log(f.name);             // f
console.log( (() => {}).name );  //  ""(空字符串)

9.函数引用表达式

注意:实际上,函数名可以为返回值为函数定义的任何表达式。

函数名;

10.函数定义作为值

函数定义(函数声明、函数表达式、箭头函数表达式)是 Function 类型的实例,与其它引用类型的值(即对象)一样,函数定义也可以作为值赋给变量或常量,保存为对象的属性或数组的元素,作为函数的实参或返回值,等等。

以下仅仅介绍部分使用场景。

10.1复制值

就跟变量的“复制值”章节一样,通过变量将一个函数对象赋给另一个变量时,此时这两个变量都指向同一个函数对象。

function sum(num1, num2) {
  return num1 + num2;
}

console.log(sum(10, 10));  // 20

let anotherSum = sum;
console.log(anotherSum(10, 10));  // 20

10.2回调函数

函数定义可以作为函数的实参和返回值,只要满足其一,我们就可以将这种编程风格称为高阶函数风格(Higher Order Function Style),函数 callSomeFunction() 称为高阶函数(Higher Order Function),函数 add() 称为回调函数(Callback Function)。

//作为实参
function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}

function add(num) {
  return num + 10;
}

console.log(callSomeFunction(add, 10));  // 20
//作为返回值
function callSomeFunction() {
  return function (num) {
    return num + 10;
  };
}

let add = callSomeFunction();
console.log(add(10));  // 20

11.形参分类

注意:调用剩余形参时,剩余形参名的前面无 ...

名称修饰符
值形参——————
默认值形参——————
剩余形参...

11.1形参作用域

值形参、默认值形参、剩余形参可以想象成与在函数体内按顺序声明变量一样。

值形参、剩余形参近似于已声明未初始化的变量,默认值形参近似于已声明已初始化的变量。

//值形参
function f(a, b) {
  console.log(a, b);
}
f();  // undefined undefined

function f() {
  let a;
  let b;
  console.log(a, b);
}
f();  // undefined undefined
//默认值形参
function f(a = 1, b = 2) {
  console.log(a, b);
}
f();  // 1 2

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

因为形参是按顺序声明的,则后定义的默认值形参可以引用先定义的形参,但先定义的默认值形参不可以引用后定义的默认值形参。

//正确
function f(a, b = a + 1) {
  console.log(a, b);
}

f(1);  // 1 2
//错误
function f(a = b - 1, b = 2) {
  console.log(a, b);
}

f();  // 报错

形参的作用域是单向的,函数体可以访问形参,但形参不可以访问函数体。

function f(a = c, b = d) {
  let c = 3;
  let d = 4;
  console.log(a, b);
}
f();  // 报错

12.函数签名

在很多其它编程语言中,函数签名(由函数名、形参类型、形参个数组成)就好比是函数的身份证一样,可以通过比较函数签名来判断是否是同一个函数。

JavaScript 语言的函数隐式地只有一个形参,arguments 就是这个形参。arguments 为一个类似于数组的对象,为了易于理解,我们可以暂且称为 arguments 数组。arguments 数组内的元素为所有传递给函数的实参,函数只有验证传递过来的是不是 arguments 数组的机制,并没有验证 arguments 数组内元素的类型和个数的机制。

函数的显式的形参只是为了方便调用才写出来的,并不是必须写出来的,可以使用类似数组中变量名后跟方括号 [] 包含索引来访问 arguments 数组中索引对应的实参。第一个实参是 arguments[0],第二个实参是 arguments[1],以此类推。

所以,JavaScript 语言的函数没有函数签名,或者说 JavaScript 语言的函数的函数签名只是函数名。

13.实参传递方式

实参的类型无需与默认值形参的类型兼容。

实参的个数可以小于、等于、大于形参的个数,甚至可以没有实参。

实参的传递只支持按位置从左往右依次传递方式,不支持按命名传递方式(可以使用对象字面量解构赋值模拟按命名传递方式)。

实参的传递只支持按值传递方式(就跟变量的“复制值”章节一样),不支持按引用传递方式。

实参类型按值传递按引用传递
原始类型函数内形参的变化不会反应到函数外的实参。不支持
引用类型函数内形参的变化会反应到函数外的实参。不支持
//按值传递(原始类型)
function addTen(num) {
  num += 10;
}

let count = 20;
addTen(count);
console.log(count);  // 20
//按值传递(引用类型)
function setName(obj) {
  obj.name = "张三";
}

let person = new Object();
setName(person);
console.log(person.name);  // 张三
//此示例是为了证明JavaScript只支持按值传递,不支持按引用传递。
function setName(obj) {
  obj.name = "张三";
  obj = new Object();
  obj.name = "赵六";
}

let person = new Object();
setName(person);
console.log(person.name);  // 张三

注意:不给值形参传递值,相当于给值形参传递了 undefined 值。不给剩余形参传递值,相当于给剩余形参传递了空数组。这就类似于变量已声明未初始化。

注意:给默认值形参不传递或传递 undefined 值,都会触发默认值形参的默认值。给默认值形参传递 null 值,不会触发默认值形参的默认值。

14.函数返回值

参见 return 语句。

15.函数调用表达式

注意:实际上,函数名可以为返回值为函数定义的任何表达式。

//无实参
函数名();

//有实参
函数名(实参);

16.函数返回方式

按值返回。

17.函数重载(Overload)

在很多其它编程语言中,可以定义两个同名函数,只要函数签名不同就可以实现函数重载。

但在 JavaScript 语言中,如果定义了两个同名函数,因为 JavaScript 语言的函数没有函数签名,此时会被认为是同一个函数,则后定义的会覆盖先定义的,所以 JavaScript 语言自然而然也就不支持函数重载。

可以只定义一个函数,然后通过检查实参的类型和数量,然后分别执行不同的逻辑来模拟函数重载。

function doAdd() {
  if(arguments.length === 1) {
    console.log(arguments[0] + 10);
  }

  if(arguments.length === 2) {
    console.log(arguments[0] + arguments[1]);
  }
}

doAdd(10);      // 20
doAdd(30, 20);  // 50

18.函数递归(Recursion)

一个函数定义的函数体内有此函数的函数调用,此时便形成了函数递归(Recursion)。

const factorial = function f(num) {
  if (num <= 1) {
    return 1;
  }
  return num * f(num - 1); 
}; 

19.闭包(Closure)

我们暂且将定义在函数体内顶级的变量、常量、函数称为成员。

JavaScript 语言允许函数嵌套,嵌套函数可以访问外围函数内定义的所有成员,同理,外围函数内定义的所有成员可以访问嵌套函数,但是外围函数内定义的所有成员不可以访问嵌套函数内定义的所有成员,这为嵌套函数内定义的所有成员提供了一种封装。

形成闭包有两个条件:

  • 嵌套函数访问了外围函数内定义的成员。
  • 嵌套函数以某种方式在外围函数之外的任何作用域被使用。
const getSomeFunc = function (name) {
  const getName = function () {
    //嵌套函数访问了外围函数内定义的变量name
    console.log(name);
  };
  return getName;
};

//嵌套函数以某种方式在外围函数之外的任何作用域被使用
//外围函数调用完之后不会被销毁,因为嵌套函数需要使用外围函数内定义的变量name
const getNameFunc = getSomeFunc("张三");
getNameFunc();  // 张三

原创文章,作者:huoxiaoqiang,如若转载,请注明出处:https://www.huoxiaoqiang.com/javascript/javascriptlang/4731.html

(0)
huoxiaoqiang的头像huoxiaoqiang
上一篇 2020年8月6日 20:07
下一篇 2020年8月8日 02:22

相关推荐

  • 2.1JavaScript语言的ES模块(Module)

    ES 模块用于在浏览器环境和服务器环境中使用。 模块就是以 .js 为扩展名的 JavaScript 文件。 普通脚本文件内的顶级的成员对其它脚本文件来说是公开(public)的全局上下文,而模块文件内的顶级的模块成员对其它模块文件来说都是私有(private)的,所以首先需要在模块中将它们显式导出,然后在其它模块中显…

  • 1.4JavaScript语言的类型(Type)

    JavaScript 语言有两种类型:原始类型(Primitive Type) 和 引用类型(Reference Type)。 原始类型包括:大整数类型(不可变的)、数值类型(不可变的)、布尔类型(不可变的)、字符串类型(不可变的)、符号类型(不可变的)、Undefined类型(不可变的)、Null类型(不可变的)。 …

  • 1.5JavaScript语言的变量(Variable)

    1.变量声明 注意:在同一个作用域,相同名称的变量不可以重复声明,否则会报错。 变量在赋值后可以通过重新赋值改变其值。 注意:新的值的类型不一定与老的值的类型相同。 2.变量声明提升(Hoisting) 变量声明不支持提升。 3.变量作用域 变量的作用域是块作用域,即外围离变量最近的代码块。全局变量的作用域是定义它们的…

发表回复

登录后才能评论