第7集丨JavaScript 中函数——概述


一般来说,一个函数是可以通过外部代码调用的一个“子程序”(或在递归的情况下由内部函数调用)。像程序本身一样,一个函数由称为函数体的一系列语句组成。值可以传递给一个函数,函数将返回一个值。在 JavaScript 中,函数是头等 ( first-class)对象,因为它们可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于函数可以被调用。简而言之,它们是 Function对象。

一、函数概览

JavaScript 中,每个函数其实都是一个Function对象。

  • 如果一个函数中没有使用 return 语句,则它默认返回undefined
  • 要想返回一个特定的值,则函数必须使用 return 语句来指定一个要返回的值。(使用new关键字调用一个构造函数除外)。

调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫作形参

  • 如果实参是一个包含原始值 (数字,字符串,布尔值) 的变量,则就算函数在内部改变了对应形参的值,返回后,该实参变量的值也不会改变。
  • 如果实参是一个对象引用,则对应形参会和该实参指向同一个对象。假如函数在内部改变了对应形参的值,返回后,实参指向的对象的值也会改变。

this关键字

  • 在函数执行时,this 关键字并不会指向正在运行的函数本身,而是指向调用该函数的对象。
  • 所以,如果你想在函数内部获取函数自身的引用,只能使用函数名或者使用arguments.callee属性 (严格模式下不可用),如果该函数是一个匿名函数,则你只能使用后者。
 /* 定义函数 myFunc */
 function myFunc(theObject)
 {
   //实参 mycar 和形参 theObject 指向同一个对象。
   theObject.brand = "Toyota";
 }

 /*
  * 定义变量 mycar;
  * 创建并初始化一个对象;
  * 将对象的引用赋值给变量 mycar
  */
 var mycar = {
   brand: "Honda",
   model: "Accord",
   year: 1998
 };

 /* 弹出 'Honda' */
 window.alert(mycar.brand);

 /* 将对象引用传给函数 */
 myFunc(mycar);

 /*
  * 弹出 'Toyota',对象的属性已被修改。
  */
 console.log(mycar.brand);

二、函数定义

定义函数有多种方法:

2.1 函数声明 (函数语句)

语法:function name([param[, param[, ... param]]]) { statements }

 test();	//正常执行,弹出1
 function test() {
     alert(1)
 }

2.2 函数表达式 (function expression)

函数表达式和函数声明非常相似,它们甚至有相同的语法。一个函数表达式可能是一个更大的表达式的一部分。可以定义函数“名字”(例如可以在调用堆栈时使用)或者使用“匿名”函数。函数表达式不会提升,所以不能在定义之前调用

语法:
var myFunction = function name([param[, param[, ... param]]]) { statements }

test2()	//不能正常执行,报错
var test2 = function() {
    alert(2)
}

在这里插入图片描述

2.3 匿名函数立即执行

当函数只使用一次时,通常使用IIFE (Immediately Invokable Function Expressions)

(function() {
    statements
})();

IIFE是在函数声明后立即调用的函数表达式。

2.4 函数生成器声明 (function* 语句)

函数声明有一种特殊的语法:
function* name([param[, param[, ...param]]]) { statements }

function* 这种声明方式 (function 关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。

function* generator(i) {
  yield i;
  yield i + 10;
}

const gen = generator(10);

console.log(gen.next().value);
// Expected output: 10

console.log(gen.next().value);
// Expected output: 20

生成器函数在执行时能暂停,后面又能从暂停处继续执行

  • 调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 (iterator)对象。
  • 当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止, yield 后紧跟迭代器要返回的值。
  • 或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。
  • next()方法返回一个对象,这个对象包含两个属性:valuedonevalue 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。
  • yield 关键字使生成器函数执行暂停,yield 关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的 return 关键字。

2.5 函数生成器表达式 (function*表达式)

语法function* [name]([param] [, param] [..., param]) { statements }

  • name:函数名。在声明匿名函数时可以省略。函数名称只是函数体中的一个本地变量。

  • paramN:传入函数的一个参数名。一个函数最多有 255 个参数。

  • statements:函数体。

function*表达式和function* 声明比较相似,并具有几乎相同的语法。function*表达式和function*声明之间主要区别就是函数名,即在创建匿名函数时,function*表达式可以省略函数名。

var x = function*(y) {
   yield y * y;
};

2.6 箭头函数表达式 (=>)

语法([param] [, param]) => { statements } param => expression

  • param:参数名称。零参数需要用 () 表示。只有一个参数时不需要括号。(例如 foo => 1)

  • statements or expression:多个声明 statements 需要用大括号括起来,而单个表达式时则不需要。表达式 expression 也是该函数的隐式返回值。

2.7 Function构造函数

不推荐使用 Function 构造函数创建函数,因为它需要的函数体作为字符串可能会阻止一些 JS 引擎优化,也会引起其他问题。

语法new Function (arg1, arg2, ... argN, functionBody)

  • arg1, arg2, … argN
    函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的 JavaScript 标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。

  • functionBody
    一个构成的函数定义的,包含 JavaScript 声明语句的字符串。

Function 的构造函数当作函数一样调用 (不使用 new 操作符) 的效果与作为 Function 的构造函数调用一样。

2.8 生成器函数的构造函数

JavaScript 中,生成器函数实际上都是 GeneratorFunction 对象。注意,GeneratorFunction 并不是一个全局对象,但你可以通过下面的代码创建 GeneratorFunction() 构造函数。

const GeneratorFunction = function* () {}.constructor;
  • 不推荐使用构造器函数的构造函数 (GeneratorFunction constructor) 创建函数,因为它需要的函数体作为字符串可能会阻止一些 JS 引擎优化,也会引起其他问题。

语法new GeneratorFunction (arg1, arg2, ... argN, functionBody)

  • arg1, arg2, … argN
    函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的 JavaScript 标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。

  • functionBody
    一个构成的函数定义的,包含 JavaScript 声明语句的字符串。

三、函数参数

3.1 默认参数

如果没有值或传入了未定义的值,默认函数参数允许形式参数使用默认值初始化。

function multiply(a, b = 1) {
  return a * b;
}

console.log(multiply(5, 2));
// Expected output: 10

console.log(multiply(5));
// Expected output: 5

3.2 剩余参数

剩余参数语法允许将数量不限的参数描述成一个数组。

function sum(...theArgs) {
  let total = 0;
  for (const arg of theArgs) {
    total += arg;
  }
  return total;
}

console.log(sum(1, 2, 3));
// Expected output: 6

console.log(sum(1, 2, 3, 4));
// Expected output: 10

四、arguments对象

arguments 对象是所有(非箭头)函数中都可用的局部变量(一个包含了传递给当前执行函数参数的类似于数组的对象)。你可以使用arguments 对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引 0 处。

  • arguments.callee 已弃用: 当前正在执行的函数。
  • arguments.caller已弃用 : 调用当前执行函数的函数。
  • arguments.length: 传给函数的参数的数目。

五、方法函数定义

5.1 Getter 和 setter 函数

你可以在支持添加新属性的任何标准的内置对象或用户定义的对象内定义 getter(访问方法) 和 setter(设置方法)。使用对象字面量语法定义 getterssetters 方法。

  • get
    当查找某个对象属性时,该对象属性将会与被调用函数绑定。
const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    return this.log[this.log.length - 1];
  }
};

console.log(obj.latest);
// Expected output: "c"
  • set
    当试图设置该属性时,对象属性与被调用函数绑定。
const language = {
  set current(name) {
    this.log.push(name);
  },
  log: []
};

language.current = 'EN';
language.current = 'FA';

console.log(language.log);
// Expected output: Array ["EN", "FA"]

5.2 方法定义语法

ECMAScript 6 开始,你可以用更短的语法定义自己的方法,类似于 getterssetters

 var obj = {
   foo() {},
   bar() {}
 };

六、构造函数 vs 函数声明 vs 函数表达式

6.1 对比案例

对比下面的例子:

  • 一个用Function构造函数定义的函数,被赋值给变量 multiply
var multiply = new Function('x', 'y', 'return x * y');
  • 一个名为multiply的函数声明:
function multiply(x, y) {
   return x * y;
} // 没有分号
  • 一个匿名函数的函数表达式,被赋值给变量multiply
 var multiply = function(x, y) {
   return x * y;
 };
  • 一个命名为func_named的函数的函数表达式,被赋值给变量multiply:
var multiply = function func_name(x, y) {
   return x * y;
};

6.2 差别

虽然有一些细微的差别,但所起的作用都差不多:

6.2.1 函数名和函数的变量

  • 函数名和函数的变量存在着差别。函数名不能被改变,但函数的变量却能够被再分配。
  • 函数名只能在函数体内使用。倘若在函数体外使用函数名将会导致错误。(如果函数之前是通过一个 var 语句声明的则是 undefined)。例如:
var y = function x() {};
alert(x); // throws an error
  • 当函数是通过 Function’s toString() 方法被序列化时,函数名同样也会出现。

  • 另一方面,被函数赋值的变量仅仅受限于它的作用域,该作用域确保包含着该函数被声明时的作用域。正如第四个例子所展示的那样,函数名与被函数赋值的变量是不相同的。彼此之间没有关系。函数声明同时也创建了一个和函数名相同的变量。因此,与函数表达式定义不同,以函数声明定义的函数能够在它们被定义的作用域内通过函数名而被访问到:

6.2.2 new Function 定义的函数

  • 使用用 new Function 定义的函数没有函数名。 然而,在 SpiderMonkey JavaScript 引擎中,其函数的序列化形式表现的好像它拥有一个名叫"anonymous"的名称一样。比如,使用 alert(new Function()) 输出:
function anonymous() {
}
  • 而实际上其函数并没有名称,anonymous 不是一个可以在函数内被访问到的变量。例如,下面的例子将会导致错误:
 var foo = new Function("alert(anonymous);");
foo(); // Uncaught ReferenceError: anoymous is not defined

6.2.3 函数声明的优势

  • 和通过函数表达式定义或者通过 Function 构造函数定义的函数不同,函数声明定义的函数可以在它被声明之前使用。举个例子:
foo(); // alerts FOO!
function foo() {
   alert('FOO!');
}

6.2.4 三者对比

  • 函数表达式定义的函数继承了当前的作用域。换言之,函数构成了闭包。另一方面,Function 构造函数定义的函数不继承任何全局作用域以外的作用域 (那些所有函数都继承的)。

  • 通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而 Function 构造函数定义的函数却不同。也就是说,每次构造函数被调用,传递给 Function 构造函数的函数体字符串都要被解析一次。虽然函数表达式每次都创建了一个闭包,但函数体不会被重复解析,因此函数表达式仍然要快于"new Function(...)"。所以 Function 构造函数应尽可能地避免使用。

  • 有一点应该要注意的,在通过解析 Function 构造函数字符串产生的函数里,内嵌的函数表达式和函数声明不会被重复解析。例如:

var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
foo(); // 函数体字符串"function() {\n\talert(bar);\n}"的这一部分不会被重复解析。

6.3 举例

// 函数声明
function foo() {}

// 函数表达式
(function bar() {})

// 函数表达式
x = function hello() {}

if (x) {
   // 函数表达式
   function world() {}
}

// 函数声明
function a() {
   // 函数声明
   function b() {}
   if (0) {
      //函数表达式
      function c() {}
   }
}

七、块级函数

ECMAScript 6 开始,在严格模式下,块里的函数作用域为这个块。ECMAScript 6 之前不建议块级函数在严格模式下使用。

'use strict';

function f() {
  return 1;
}

{
  function f() {
    return 2;
  }
}

f() === 1; // true

// f() === 2 in non-strict mode

7.1 非严格模式下的块级函数

一句话:不要用。

在非严格模式下,块中的函数声明表现奇怪。例如:

if (shouldDefineZero) {
   function zero() {     // DANGER: 兼容性风险
      console.log("This is zero.");
   }
}

ECMAScript 6 中,如果shouldDefineZerofalse,则永远不会定义 zero,因为这个块从不执行。然而,这是标准的新的一部分。这是历史遗留问题,无论这个块是否执行,一些浏览器会定义 zero

在严格模式 下,所有支持 ECMAScript 6 的浏览器以相同的方式处理:只有在 shouldDefineZerotrue 的情况下定义 zero,并且作用域只是这个块内。

有条件地定义一个函数的一个更安全的方法是把函数表达式赋给一个变量:

var zero;
if (0) {
   zero = function() {
      console.log("This is zero.");
   };
}

八、示例

8.1 返回格式化数字

下面的函数返回一个字符串,其中包含了一个格式化的、以一个由 0 开头并填充的数字。

// 这个函数返回一个由 0 开头并填充的字符串
function padZeros(num, totalLen) {
   var numStr = num.toString();             // 用字符串返回值进行初始化
   var numZeros = totalLen - numStr.length; // 计算 zeros 顺序
   for (var i = 1; i <= numZeros; i++) {
      numStr = "0" + numStr;
   }
   return numStr;
}

//执行
var result;
result = padZeros(42,4); // returns "0042"
result = padZeros(42,2); // returns "42"
result = padZeros(5,4);  // returns "0005"

8.2 检测函数是否存在

你可以通过 typeof 操作符检测一个函数是否存在。在下面的例子中,用一个测试来演示检测 window 对象是否拥有一个 noFunc 函数的属性。如果存在,那就使用它;否则就采取其他的一些操作。(注意在 if 语句中,使用了 noFunc 的引用 – 在函数名的后面没有括号“()”,所以实际函数并没有被调用。)

if ('function' === typeof window.noFunc) {
   // use noFunc()
 } else {
   // do something else
 }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值