JavaScript学习-函数进阶

理解函数调用

Posted by MakeSail on November 3, 2019

函数进阶

该文章主要讨论一下三点:

  • 函数中两个隐含的参数:arguments和this
  • 调用函数的不同方式
  • 处理函数上下文的问题

理解函数中两个隐含的参数

隐式的函数参数this和arguments会被静默的传递给函数,并且可以像函数体内显示声明的参数一样被正常访问。

理解this

参数this表示被调用函数的上下文对象,其是JavaScript面向对象编程的基本要素之一,代表函数调用相关联的对象。

函数调用

我们可以通过四种方式来调用一个函数

  • 作为一个函数(function)—- skulk(),直接被调用
  • 作为一个方法(method)— ninja.skulk(),关联在一个对象上,实现面向对象编程。
  • 作为一个构造函数(constructor)—- new Ninja(),实例化一个新的对象。
  • 通过函数的apply或者call方法—-skulk.apply(ninja)或者skulk.call(ninja);
function Ninja(name){
    
}
function skulk(name){
    
}

skulk('Hattori');

// 作为函数直接调用
(function(who){
    return who;
})('Hattori');

var ninja = {
    skulk:function(){}
};
// 作为ninja对象的一个方法调用
ninja.skulk('Hattori'); 

// 作为构造函数调用
ninja = new Ninja('Hattori');

// 通过call方法调用
skulk.call(ninja,'Hattori');

// 通过apply方法调用
skulk.apply(ninja,['Hattori']);
作为函数直接被调用

通过()运算符调用一个函数,且被执行的函数表达式不是作为一个对象的属性存在时候,就属于这种调用类型。

当以这种方式调用的时候,函数上下文(this关键字的值)有两种可能性:在非严格模式下,它将是全局上下文(window对象),而在严格模式下,它将是undefined。

// 以下是在chrome浏览器开发者工具中书写
function ninja(){
    console.log(this);
}
function samurai(){
    'use strict'
    console.log(this);
}

ninja()
samurai()

VM342:2 Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
VM342:6 undefined
作为方法被调用

当一个函数被赋值给一个对象的属性,并且通过对象属性引用的方式调用函数时,函数会作为对象的方法被调用。

var ninja = {};

ninja.skulk = function(){};

ninja.skulk();

当函数作为某个对象的方法被调用时,该对象会成为函数的上下文,并且在函数内部可以通过参数访问到。这是JavaScript实现面向对象变成的主要方式之一。

函数调用和方法调用的区别

function whatMyContext(){
    return this;
}
console.log(whatMyContext() === window,"Function call on window");

let getMyThis = whatMyContext; // 变量getMyThis得到了函数whats MyConText的引用

// 使用变量getMyThis来调用函数,该函数仍然作为函数被调用,函数上下文也依然是window对象
console.log(getMyThis() === window,"Another function call in window");

// 创建一个对象ninja1,其属性getMyThis得到了函数whatsMyContext的应用
const ninja1 = {
    getMyThis: whatMyContext
};

// 使用ninja1对象的方法getMyThis来调用函数。函数的上下文现在是Ninja1了。
console.log(ninja1.getMyThis() === ninja1,'Working with 1st ninja');

作为构造函数被调用

若要通过构造函数的方式调用,需要在函数调用之前使用关键字new。

将函数作为构造函数调用是JavaScript中的一个强大的特性,一般来讲,当调用构造函数时会发生一系列特殊的操作。使用关键字new调用函数会触发以下几个动作。

  1. 创建一个新的空对象
  2. 该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文。
  3. 新构造的对象作为new运算符的返回值

构造函数的目的是创建一个新对象,并进行初始化设置,然后将其作为构造函数的返回值。

构造函数返回值

  • 如果构造函数返回一个对象,则该对象作为整个表达式的值返回,而传入构造函数的this将被丢弃。
  • 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。

编写构造函数的注意事项

构造函数的目的是根据初始化条件对函数调用创建的新对象进行初始化。虽然这些函数也可以被“正常”调用,或者被赋值为对象属性从而作为方法调用,但这样没有什么意义。

因为构造函数通常以不同与普通函数的方式来编码和使用,并且只有作为构造函数调用时才有意义,因此出现了命名约定来区分构造函数与普通函数以及方法。

函数和方法的命名通常以描述其行为(skulk,creep,sneak)的动词开头,且第一个字母小写。而构造函数则通常以描述所构造对象的名词命名,并以大写字母开头。

使用apply和call方法调用

迄今为止,你应该已经注意到,不同类型的函数调用之间的主要区别在于:最终作为函数上下文(可以通过this参数隐式引用到)传递给执行函数的对象不同,对于方法而言,即为方法所在的对象;对于顶级函数而言是window或者undefined;对于构造函数而言是一个新创建的对象实例。

值得注意的是,apply和call之间唯一的不同在于如何传递参数。在我们使用apply的情况下,我们使用参数数组;在使用call的情况下,我们则在函数上下文之后依次列出调用参数。

call和apply这两个方法对于我们要特殊指定一个函数的上下文对象时特别有用,在执行回调函数时可能会经常用到。

强制指定回调函数的函数上下文

使用牵头函数绕过函数上下文

箭头函数作为回调函数还有一个更优秀的特性:箭头函数没有单独的this值。箭头函数的this与声明所在的上下文相同。

调用箭头函数时,不会隐式传入this参数,而是从定义时的 函数继承上下文。

理解arguments

参数arguments参数表示函数调用过程中传递的所有参数,通过arguments参数我们可以访问函数调用过程中 传递的实际参数。借用它,我们可以实现原生JavaScript并不支持的 重载特性,而且可以实现接收数量可变的可变函数。

注意,arguments参数并不是数组。

可以看到,arguments对象的主要作用是允许我们访问传递给函数的所有参数,即便部分参数没有和函数的形参关联也无妨。

function whatever(a,b,c){

    console.log(a === 1, 'The value of a is 1');
    console.log(b === 2, 'The value of b is 2');
    console.log(c === 3, 'The value of c is 3');
    for(let i = 0;i<arguments.length;i++){
        console.log(arguments[i]);
    }
    console.log(arguments.length === 5,
        'We’ve passed in 5 parameters');
    console.log(arguments[0] === a,
        'The first argument is assigned to a');
    console.log(arguments[1] === b,
        'The second argument is assigned to b');
    console.log(arguments[2] === c,
        'The third argument is assigned to c');
    console.log(arguments[3] === 4,
        'We can access the fourth argument');
    console.log(arguments[4] === 5,
        'We can access the fifth argument');
}
whatever(1,2,3,4,5);
注意:在大多数情况下可以使用剩余参数来代替arguments参数。剩余参数是真正的Array实例,也就是说可以在它上面直接使用所有的数组方法。

同时,arguments参数有一个有趣的特性:它可以作为函数参数的别名。例如,如果为arguments[0]赋一个新的值,那么,同时也会改变第一个参数的值。