JavaScript函数详细介绍

4种实例化函数对象的方式

1. 函数声明,函数定义最后没有分号

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

2. 函数表达式,函数表达式创建的函数的末尾是有分号的,与任何变量初始化语句一样。

//定义了一个变量sum并将其初始化为一个函数。注意function关键字后面没有名称,因为不需要
//这个函数可以通过sum变量来引用
//
let sum = function (num1,num2){
  return num1 + num2;
};

3. 箭头函数(arrow function)

let sum = (num1, num2) => {
  return num1 + num2;
}

4. 使用Function构造函数。

这个构造函数可以接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数

let sum = new Function("num1",,"num2","return num1 + num2");//不推荐

不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它作为常规ECMAScript代码,第二次是解释传给构造函数的字符串。会影响性能。

这几种实例化函数对象的方式之间存在微妙但重要的差别,后续会讨论。无论如何,通过其中任何一种方式都可以创建函数。

箭头函数

任何可以使用函数表达式的地方,都可以使用箭头函数:

let arrowSum = (a, b) => { return a + b; }
let functionExpressionSum = function (a, b) { return a + b; }
console.log(arrowSum(1, 2));//3
console.log(functionExpressionSum(1, 2))//3

 箭头函数简洁的语法非常适合嵌入函数的场景:

let ints =[1,2,3];
console.log(ints.map(i) => {return i + 1}); //[2,3,4]

如果只有一个参数,可以不用小括号,没有参数时,或者多个参数的情况下,才需要使用括号:

//有且仅有一个参数,小括号可以省略
let double = (x) => return 2 * x;
let triple = x => return 2 * x;

//当有多个参数或者没有参数必须加小括号
let sum = () => {return 1 + 1};
let sum1 = (x,y) => {return x + y};

//多个参数不加小括号等于无效写法
let sum1 = x,y => {return x + y};

箭头函数也可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规函数一样。

如果不使用大括号,那么箭头函数后面就只能有一行代码并且可以省略return,该行代码执行的结果就是箭头函数的返回值

let double = (x) => x * 2;
double(x);//4

箭头函数虽然语法简单,但也有很多场景不适用。

箭头函数不能使用argumentssupernew.target,也不能用作构造函数,并且没有prototype属性

函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称

function sum(num1, num2) {
    return num1 + num2;
}
console.log(sum(10, 10));//20

let anotherSum = sum;
console.log(anotherSum(10, 10));//20
// @ts-ignore
sum = null;
console.log(anotherSum(10, 10));//20

以上代码定义了一个名为sum()的函数,用于求两个数之和。然后又声明了一个变量anotherSum,并将它的值设置为等于sum。注意,使用不带括号的函数名会访问函数指针,而不会执行。此时,anotherSum和sum都指向同一个函数。调用anotherSum()也可以返回结果。把sum设置为null之后,就切断了它与函数之间的关联。而anotherSum()还是可以正常调用。

ECMAScript 6的所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用Function构造函数创建的,则会标识成:“anonymous”

function foo() { };
let bar = function () { };
let baz = () => { };

console.log(foo.name); // 输出 "foo"
console.log(bar.name); // 输出 "bar"
console.log(baz.name); // 输出 "baz"
console.log((() => { }).name);//(空字符串)
console.log((new Function()).name);//anonymous

 如果函数是一个获取函数,设置函数或者使用bind()实例化,那么标识符前面会加上一个前缀:

function foo() { }
console.log(foo.bind(null).name);// "bound foo"

let dog = {
    years: 1,
    get age() {
        return this.years;
    },
    set age(newAge) {
        this.years = newAge;
    }
}

let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name); // "get age"
console.log(propertyDescriptor.set.name); // "set age"

 Object.getOwnPropertyDescriptor() 静态方法返回一个对象,该对象描述给定对象上特定属性(即直接存在于对象上而不在对象的原型链中的属性)的配置。返回的对象是可变的,但对其进行更改不会影响原始属性的配置。

//语法:
Object.getOwnPropertyDescriptor(obj, prop)
// 参数:
// obj
// 要查找其属性的对象。
// prop
// 要检索其描述的属性的名称或 Symbol。
const object1 = {
  property1: 42,
};

const descriptor1 = Object.getOwnPropertyDescriptor(object1, 'property1');

console.log(descriptor1.configurable);
// Expected output: true

console.log(descriptor1.value = 40); //40
console.log(object1.property1);		 //42

 理解参数

ECMAScript函数的参数跟大多数其他语言不同。ECMAScript函数既不用关心传入的参数个数,也不关心这些参数的类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。可以传一个,两个,甚至一个不传,解释器都不会报错。

之所以会这样,主要是因为ECMAScript函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元素超出了要求,也没问题。事实上,在使用function关键字定义(非箭头函数)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。

arguments对象是一个类数组对象(但不是Array的实例),因此可以使用中括号语法访问其中的元素(第一个参数是:arguments[0],以此类推)。而要确定传进来多少个参数,可以访问arguments.length属性。

function sayHi(name, message) {
    console.log("Hello " + name + "! " + message);
}
// 可以通过arguments[0]取得相同的参数值。因此,把函数重写成不声明参数也可以:
function sayHi() {
    console.log("Hello " + arguments[0] + "! " + arguments[1]);
}

也可以通过arguments对象的length属性检查传入的参数个数。

还有一个必须理解的重要方面,那就是arguments对象可以跟命名参数一起使用

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

在这个doAdd()函数中,同时使用了两个命名参数和arguments对象。命名参数num1保存着与arguments[0]一样的值,因此使用谁都无所谓。

arguments对象的另一个有意思的地方就是,它的值始终会与对应的命名参数同步:

function doAdd(num1, num2){
  arguments[1] = 10;
  console.log(arguments[0] + num2);
}

这个doAdd()函数把第二个参数的值重写为10。因为arguments对象的值会自动同步到对应的命名参数,所以修改arguments[1]也会修改num2的值,因此两者的值都是10.但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。另外还要记住一点:如果只传了一个参数,然后把arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为arguments对象的长度是根据调用函数时传递的参数个数,而不是函数定义时设置的命名参数的个数确定的。

对于命名参数而言,如果调用函数时没有传入这个参数,那么它的值就是undefined。这就类似于定义了变量而没有初始化。

严格模式下,arguments会有一些变化。首先,像前面那样给arguments[1]赋值不在影响num2.就算把arguments[1]设置为10,num2的值仍然是传入的值。其次,在函数中尝试重写arguments对象会导致语法错误。(代码也不会执行)

箭头函数中的参数 

 如果函数是使用箭头函数创建的函数,那么传给函数的参数就不能使用arguments关键字访问,而只能通过定义的命名参数访问

let bar = () => {
  console.log(arguments[0]);
}
bar(5);//ReferenceError: arguments is not defined

虽然箭头函数中没有arguments对象,但可以在包装函数中把它提供给箭头函数:

function foo(){
  let bar = () => {
    console.log(arguments[0]);
  }
  bar();
}
foo(5);

注意:

ECMAScript中的所有参数都是按值传递的。不可能按引用传递。如果把对象作为参数传递,那么传递的值就是这个对象的引用。

没有重载

ECMAScript函数不能像传统那样重载。在其他语言比如Java中,一个函数可以有两个定义,

只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

如果在ECMAScript中定义了两个同名函数,则后定义的会覆盖先定义的。

function add(num){return num +100;}
function add(num){return num +200;}

let result = add(100);//300

这里函数add()被定义了两次。最后返回300是因为第二个定义覆盖了第一个定义。

前面也提到过,可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟重载

注意:函数声明创建的函数和使用var关键字声明的变量都存在变量声明,但是函数声明提升的优先级更高(后执行),并且函数提升不会被同名变量提升所覆盖,但是会被同名变量赋值所覆盖

默认参数值

在ECMAScript5.1及以前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,

如果是则意味着没有传递这个参数,那就给它赋一个值:

function makeKing(name) {
    name = (typeof name !== 'undefined' ? name : 'Henry');
    return `King ${name} VIII`;
}

console.log(makeKing()); // King Henry VIII
console.log(makeKing('Louis')); // King Louis VIII

ECMAScript6之后就不用这么麻烦了,因为它支持显示定义默认参数了。下面代码就是与前面代码等价的ES6写法,只要在函数定义中的参数后面用=就可以为参数赋一个默认值:

function makeKing(name = 'Henry') {
    name = (typeof name !== 'undefined' ? name : 'Henry');
    return `King ${name} VIII`;
}

console.log(makeKing()); // King Henry VIII
console.log(makeKing('Louis')); // King Louis VIII

在使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。当然,跟ES5严格模式一样,修改命名参数也不会影响arguments对象,它始终以调用函数时传递的参数为准:

function makeKing(name = 'Henry') {
  name = 'Louis';
  return `King ${arguments[0]}`;
}
console.log(makeKing());
console.log(makeKing('Louis'));//King Louis

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值

注意:

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。

计算默认值的函数只有在调用函数但未传相应参数时才会被调用

箭头函数同样也可以使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了

默认参数作用域与暂时性死区

因为在求默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。

给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样。

function makeKing(name = 'Henry', numerals = 'VIII') {\
  return `King ${name} ${numerals}`;
}
console.log(makeKing());// King Henry VIII

//这里的默认参数会按照定义它们的顺序依次被初始化。
//可以依照如下示例想象一下过程:
function() {
  let name = 'Henry';
  let numerals = 'VIII';
  return `King ${name} ${numerals}`;
}

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数

参数初始化顺序遵循"暂时性死区"规则,即前面的参数默认值不能引用后定义的参数。像这样就会抛出错误:

//调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {\
  return `King ${name} ${numerals}`;
}

参数也存在于自己的作用域中,它们不能引用函数体的作用域:

//调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII';
}

参数扩展与收集

扩展参数(调用函数时使用)

在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素

假设有如下函数定义,它会将所有传入的参数累加起来:

let values = [1, 2, 3, 4];
function getSum() { 
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

这个函数希望把所有加数逐个传进来,然后通过迭代arguments对象来实现累加。如果不适用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于apply()方法

console.log(getSum.apply(null, values)) // 10

但在ECMAScript 6中,可以通过扩展操作符极为简单德实现这种操作。

对可迭代对象应用扩展运算符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入

console.log(getSum(...values)); //10

在普通函数和箭头函数中,也可以将扩展运算符用于命名参数,也能使用默认参数

收集参数(在函数定义时使用)

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数:

//不可以
function getProduct(...values, lastValue){}

//可以
function ignoreFirst(firstValue, ...values){
  console.log(values);
}

ignoreFirst();//[]
ignoreFirst(1);//[]
ignoreFirst(1,2);//[2]
ignoreFirst(1,2,3);//[2, 3]

函数声明与函数表达式

JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

使用函数声明创建的函数会进行函数提升,提升到当前作用域的顶端,因此可以在函数创建之前调用该函数,而函数表达式创建的函数,不会进行函数提升,必须等代码执行到它那一行,才能调用该函数

函数作为值

不仅可以把函数作为参数传递给另一个函数,而且还可以在一个函数中返回另一个函数。

从一个函数中返回另一个函数也是可以的,而且非常有用。例如,假设有一个包含对象的数组,而我们想按照任意对象的属性对数组进行排序。为此,可以定义一个sort()方法需要的比较函数,它接收两个参数,即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。这个问题可以通过定义一个根据属性名来创建比较函数的函数来解决,比如:

//利用闭包实现propName变量私有化,延长生命周期
function createComparisonFunction(propName) {
    return function (obj1, obj2) {
        var value1 = obj1[propName];
        var value2 = obj2[propName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}

let data = [
    { name: "Zachary", age: 28 },
    { name: "Nicholas", age: 29 }
]

data.sort(createComparisonFunction("name"));
console.log(data[0].name) // "Nicholas"

data.sort(createComparisonFunction("age"));
console.log(data[0].name) // "Zachary"

/* 
    sort();
    功能:对数组内的数据进行排序(默认为升序),并且返回排过序的新数组,会改变原来的数组
    根a据传递的参数进行排序。
    参数:sort(callback)如果需要按照数值排序,需要传参。sort(callback),callback为回调函数,该函数应该具有两个参数,比较这两个参数,然后返回一个用于说明这两个值的相对顺序的数字(a - b)。其返回值如下:
    若 a 小于 b,返回一个小于 0 的值。
    若 a 等于 b,则返回 0。
    若 a 大于 b,则返回一个大于 0 的值。
    原理:return的值可能是一个大于0的数也或者可能是小于等于0的数,如果return后的值大于0则让数组a和b交换一下位置,实现排序功能;小于等于0,则原来数组中的位置不变。 
*/

函数内部

在ECMAScript5中,函数内部存在两个特殊的对象:argumentsthis。ECMAScript6又新增了new.target属性。

arguments

callee 属性存在 arguments 中:指向当前arguments所在的函数

arguments是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以function关键字定义函数时才有(箭头函数创建的函数没有arguments);虽然主要用于包含函数的参数,但arguments对象其实还有一个callee属性,是一个指向arguments对象所在函数的指针。

来看这个经典的阶乘函数:

function factorial(num) {
    // 递归终止条件
    if (num === 0 || num === 1) {
        return 1;
    }
    
    // 递归调用
    return num * factorial(num - 1);
}

阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。但是这个函数要正确执行就必须保证函数名是:factorial,从而导致了紧密耦合。使用arguments.callee就可以让函数逻辑与函数名解耦:

function factorial(num) {
    // 递归终止条件
    if (num === 0 || num === 1) {
        return 1;
    }

    // 递归调用
    return num * arguments.callee(num - 1);
}

 这个重写之后的factorial()函数已经用arguments.callee代替了之前硬编码的factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。考虑下面的情况:

let trueFactorial = factorial;
factorial = function() {return 0;}
console.log(trueFactorial(5));//120
console.log(factorial(5));//0

this

this在标准函数和箭头函数中有不同的行为。

在标准函数中,this引用的是把函数当成方法调用的上下文对象

箭头函数this永远是静态的,永远指向箭头函数声明时所在作用域下的this的值

普通函数直接调用,this指向window

以对象的方法进行调用,this指向调用方法的对象

在DOM事件处理函数中,this指向触发事件的元素

注意:

函数名只是保存指针的变量。因此全局定义的函数add和o.add函数是同一个函数,只不过执行的上下文不同

caller

ECMAScript5会给函数对象添加一个属性:caller。这个属性引用的是调用当前函数的函数对象,或者如果是在全局作用域中调用的则为null

new.target

ECMAScript中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。

ECMAScript6新增了检测函数是否使用new关键字调用的new.target属性。如果函数是正常调用的,则new.target的值是undefined;如果是使用new关键字调用的,则new.target将引用被调用的构造函数

function King() {
    if (!new.target) {
        throw 'King must be instantiated using new';
    }
    console.log('King instantiated using new')
}

new King(); // King instantiated using new
King();   // Error: King must be instantiated using new

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值