JavaScript:函数

随着代码长度的增加必定会出现重复执行相同步骤的代码,其实可以不用多次重复执行这些过程,而是将这些相同的步骤打包成可重复使用的代码块,即函数。

函数就是打包在一起的多行代码,在需要时重复使用它们,大多数情况下函数会被传入一些数据,对数据进行操作然后返回更改过的数据。

function welcomeTips(name){
  console.log("欢迎" + name);
};

welcomeTips("李如松");

返回:
"欢迎光临,李如松"

代码复用就体现在只要调用函数名并以参数传入客人名就能打印和字符串语句拼接的功能。

参数

在函数中,参数不是必须使用的,不需要参数时留空,而需要多个参数时以逗号隔开即可。

// 没有参数的函数
function sayHello(){ 
    let message = "Hello"
    console.log(message);
};

sayHello();

Parameter 和 Argument 都是参数:

  • 形参(Parameter)始终出现在函数声明中,属于变量,用于存储传递到函数中的数据,并供函数使用。
  • 实参(Argument)则是一个值(字符串、数字、布尔值等等都可以),始终出现在函数调用代码(括号)中,是当函数被调用时传递到函数中的实际数据。
function welcomeTips(name,location){ // Parameter 形式参数 形参
  console.log("欢迎,来自" + location + "的" + name);
};

welcomeTips("李如松","辽东"); // Argument 实际参数 实参

返回语句与返回值

返回语句 return

所有函数都会返回某个值给调用者,例如使用 console.log('hi') 向控制台输出了字符串 hi 以外还能看到一个返回值 undefined,这是因为实际上是使用了 console.log 这个函数并向其传递了参数。

function welcomeTips(name) {
  return "欢迎光临," + name; // 使用关键字 `return` 可以将函数的操作结果返回
};

console.log(welcomeTips("李如松"));

且注意,运行 'return' 后后续代码将不会执行。

所以调试时可以使用此方法打断后续操作,另外要小心代码格式化工具是有可能将 'return' 格式化后将后续代码换行造成问题,比如我就被 atom-beautify 坑过...

function sayTest() {
    return;
    console.log('test');
}
sayTest();

console.log 和 return

输出和返回并不是一回事。因此重点是可以通过两种不同的方式让函数产生某种输出,第一种方式是使用 console.log 输出消息,第二种方式是使用关键字 return 和一个值向调用者返回某个值。这里要指出的两个要点是关键字 return 用于停止函数的执行,它返回一个值给调用者,如果没有定义返回值则会自动返回 undefined

function inputAndReturn() {
  console.log("我是打印输出");
  return "我是返回,我两不一样";
}
inputAndReturn();

使用返回值

返回值不仅是利用函数操作得到结果,还可以加以利用比如下面的例子

// 返回两个数字的总和
function add(x, y) {
  return x + y;
}

// 返回数字除以2的值
function divideByTwo(num) {
  return num / 2;
}

let sum = add(5, 7); // 调用 add 函数并将返回值存储在 sum 变量中
let average = divideByTwo(sum); // 调用 divideByTwo 函数并存储在 average 变量中

console.log(sum,average);

小结

  • console.log 输出打印和 return 返回是两回事
  • 函数总会有一个返回值,可以使用关键字 return 并指定返回值返回结果
  • 如果未使用关键字 returnreturn 未指定返回值则返回 undefined
  • 关键字 return 运行后后续代码便不会执行

作用域

在几乎任何编程语言中,除了常规的语法错误,许多编码错误都围绕着作用域出现。在谈论作用域时,指的是某个特定标识符比如变量名或函数名,在部分程序片段中是否可见或可被访问。

在 JavaScript 中有三种不同类型的作用域:

  • 全局作用域
  • 函数作用域
  • 块作用域

全局作用域

如果在所有函数之外定义标识符,它会被视为全局作用域的一部分,这表示该变量可以在程序中的任何位置被访问,即它是全局可用的,程序中的所有函数都可以访问在全局作用域内定义的变量。

函数作用域

如果标识符被定义在函数内部,它在该函数内部的所有位置都可见,即使是该函数内部的其他函数。

块作用域暂且不表,先说全局和函数作用域。

let globalScope = "我是全局作用域";
console.log(globalScope); // 正常;
// console.log(outsideFunctionScope,insideFunctionScope); // 出错;

function outsideFunction() {
    let outsideFunctionScope = "我是外层函数作用域";
    console.log(globalScope); // 正常;
    console.log(outsideFunctionScope); // 正常;
    // console.log(insideFunctionScope); // 出错;

    function insideFunction(){
        let  insideFunctionScope = "我是内层函数作用域";
        console.log(globalScope,outsideFunctionScope,insideFunctionScope);
    }
    insideFunction();
}

outsideFunction();

我将上面例子中会出错的例子注释掉了,如果你消除注释运行代码会看到类似这样的提示:「ReferenceError: functionScope1 is not defined详细了解

原因在于当 JavaScript 引擎尝试查找标识符时,它首先查看该标识符是否在当前函数中定义如果它在当前函数中没有找到该标识符时则查看外面一层函数,然后是再外面一层函数 以此类推,它将持续查看外层函数直至不再在任何函数内,这意味着它已到达全局作用域,如果 JavaScript 引擎检查了所有外层函数和全局作用域,并且仍未能找到标识符则会返回错误。也就是一个从当前向外查找的过程。

覆盖

作用域可能是一个棘手的难题,特别是在平衡全局作用域和函数作用域时,作用域可能导致的陷阱之一是作用域覆盖或遮蔽

let message = "我在全局中";
console.log(message);
function sayMessage() {
    message = "我在函数里";
    // let message = "我在函数里";
    console.log(message);
}
sayMessage();
console.log(message);

当执行上述代码后会发现最后一个在全局作用域下的变量 message 也变成了在函数内消息,这就是「覆盖」问题,因为在执行函数 sayMessage 后变量 message 被重新赋值,所以即使离开了函数作用域消息也被改变了,所以为了防止这种情况,在函数作用域内的同名变量需要重新创建一个新的变量而不是直接进行赋值。

你可能会想作用域看起来真麻烦,那把所有变量都放在全局作用域不就可以避免找不到的问题了么?在一开始构建小型程序的情况而言这样确实方便,但日后项目逐渐庞大时总会难以避免有同名的变量从而产生冲突,而且代码复杂庞大起来时追踪变量会比较麻烦。

小结

  • 如果标识符在全局作用域内声明,则可以随处访问。
  • 如果标识符在函数作用域内声明,则可以在所声明的函数内访问(甚至可以在函数中声明的函数内访问)。
  • 尝试访问标识符时,JavaScript 引擎将首先查看当前函数。如果没找到任何内容,则继续查看上一级外部函数,看看能否找到该标识符。将继续这么寻找,直到到达全局作用域。
  • 全局作用域不是很好的做法,可能会导致糟糕的变量名称,产生冲突的变量名称和很乱的代码。

提升

在大部分编程语言中调用函数前必须先声明函数,代码读取顺序基本上是从上到下的。

testFunction();

function testFunction() {
    console.log('测试函数');
}

但在 JavaScript 中如上例子却是可用的,这是因为 JavaScript 中十分奇特的「提升(hoisting) 」特性,它涉及到 JavaScript 代码被解释的方式,基本上在执行任何代码之前所有函数声明都被提升到当前作用域的顶部,这一特性也存在变量中但稍许不同。

sayHello();

function sayHello() {
  console.log(message);
  var message = '你好';
}

返回:
undefined

这是因为实际上代码执行是这样的

sayHello();

function sayHello() {
    var message; // 变量也提升到了作用域的顶部。
  console.log(message);
  message = '你好'; // 但是赋值仍旧在老地方。
}

所以为了避免这种错误变量的声明及赋值尽量在顶部。

小结

  • JavaScript 会将函数声明和变量声明提升到当前作用域的顶部。
  • 变量的赋值操作不会提升。
  • 在脚本的顶部声明函数和变量,这样语法和行为就会相互保持一致。

函数表达式

变量几乎可以存储所有内容,当将函数存入变量时就称为函数表达式。

let sayHello = function() { // 此时函数代码块就不需要函数命名所以这变成一个匿名函数
    console.log('Hello');
}

sayHello(); // 和函数声明一样的调用方式。

console.log(sayHello); // 访问变量而不是以函数的形式调用它时会发现它返回了函数的主体

函数表达式和提升

同样的,函数也是赋值在变量里的,所以使用函数表达式时就需要注意「提升」的问题。

函数表达式模式

回调函数

可以将函数存储在变量中使我们能够轻松地将函数传递到另一个函数中。传递到另一个函数中的函数叫做回调。

let reformatYear = function(number) {
    return number + "年";
};

function sayHello(callbackFunction) {
    let date = new Date();
    let year = date.getFullYear();
    return `欢迎来到` + callbackFunction(year);
}

console.log(sayHello(reformatYear));

有名称的函数表达式

其实函数表达式里的函数也可以是函数声明,但是没有意义,因为调用函数表达式里的函数声明时会提示错误

let sayHello = function sayHi(){
    console.log('Yeah!')
};
sayHello(); // 正常执行
sayHi(); // ReferenceError: sayHi is not defined[详细了解]

内嵌函数表达式

内嵌函数表达式是指将函数赋值到变量,然后将变量作为参数传递给其他函数。

// 将函数 displayFavorite 分配给变量 favoriteMovie 的函数表达式
let favoriteMovie = function displayFavorite(movieName) {
  console.log("我最喜欢的影片是" + movieName);
};

// 函数声明有两个参数:一个显示消息函数和一个电影名称
function movies(messageFunction, name) {
  messageFunction(name);
}

// 调用 movies 函数,传入 favoriteMovie 函数和电影名称
movies(favoriteMovie, "英雄本色II");

这样可能会有点绕,还可以再简化

// 函数声明需要两个参数:一个显示消息的函数,以及一个电影名称
function movies(messageFunction, name) {
  messageFunction(name);
}

// 调用 movies 函数,传入一个函数和电影名称
movies(function displayFavorite(movieName) {
  console.log("我最喜欢的影片是" + movieName);
}, "英雄本色II");

在上述例子上调用 movies 函数时的第一个实参是一个匿名内嵌函数表达式,因为这个函数是一次性的,不存在之后在别的地方服用的可能,而第二个参数是字符串。

这种模式在 JavaScript 中经常用到,有助于简化代码,刚开始可能会不太适应,慢慢熟练就好。

Conners Hua

这个家伙很懒,什么都没有留下。

您可能还喜欢...

发表评论

电子邮件地址不会被公开。 必填项已用*标注