# 函数的深入浅出
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代码运行过程中,形参
的作用为接收实参
,它们两个分别位于不同的内存地址中
,大致可以分为两种情况:
实参
为原始值
。当实参为原始值时,此时形参
为实参
的拷贝
。因此,函数体内形参值
的改变并不会影响实参
。
function test(str) {
str = 'chinese';
return str;
}
const str1 = 'china';
const str2 = test(str1);
console.log(str1); // china
console.log(str2); // chinese
实参
为引用值
。当实参为引用值时,此时形参为实参内存地址的拷贝
。因此,函数体内形参值的变化
在一定情况
下将会影响实参
。
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)
← 搞清事件循环、宏任务、微任务 内存管理 →