彻底理解JavaScript函数的调用方式和传参方式——结合经典面试题

6,623 阅读9分钟

JavaScript函数的调用方式和传参方式

了解函数的调用过程有助于深入学习与分析JavaScript代码。

在JavaScript中,函数是一等公民,函数在JavaScript中是一个数据类型,而非像C#或其他描述性语言那样仅仅作为一个模块来使用。函数有四种调用模式,分别是:函数调用形式、方法调用形式、构造器形式以及apply和call调用形式。这里所有模式中,最主要的区别在于关键字this的意义,下面分别介绍这几种调用形式。(注以下代码都是运行在浏览器环境中) 本文的主要内容:

  1. 分析函数的四种调用形式
  2. 弄清楚函数中this的意义
  3. 明确构造函数对象的过程
  4. 学会使用上下文调用函数

一、函数调用形式

函数调用形式是最常见的形式,也是最好理解的形式。所谓函数形式就是一般声明函数后直接调用即时。例如:

function foo(){
    console.log(this);
}
foo();//Window

单独独立调用的,就是函数调用模式,即函数名(参数),不能加任何其他东西,对象o.fun()就不是了。在函数调用模式中,this表示全局对象window。 任何自调用函数都是函数模式。

二、方法调用模式

函数调用模式很简单,是最基本的调用方式。但是同样的是函数,将其赋值给一个对象的成员以后,就不一样了。将函数赋值给对象的成员后,那么这个就不再称为函数,而应该称为方法。 所谓方法调用,就是对象的方法调用。方法是什么,方法本身就是函数,但是,方法不是单独独立的,而是要通过一个对象引导来调用。就是说方法对象一定要有宿主对象。 即对象.方法(参数)。 this表示引导方法的对象,就是宿主对象。 对比函数调用模式:

  1. 方法调用模式不是独立的,需要宿主,而函数调用模式是独立的;
  2. 方法调用模式方式:obj.fun();函数调用模式方式:fun();
  3. 方法调用模式中,this指宿主;而函数调用模式中this指全局对象window。
//定义一个函数
function foo(){
    console.log(this);
}
//将其赋值给一个对象
var o = {};
o.fn = foo;//注意这里不要加括号
//调用
o.fn();//o

函数调用中,this专指全局对象window,而在方法中this专指当前对象,即o.fn中的this指的就是对象o。 美团的一道面试题:

var length = 10;
function fn(){
    console.log(this.length);
}
var obj = {
    length:5,
    method:function(fn){
        fn();//10 前面没有引导对象,函数调用模式
        arguments[0]();//2 
        //arguments是一个伪数组对象,这里调用相当于通过数组的索引来调用
        //这里的this就是指的这个伪数组,所以this.length为2
    }
};
obj.method(fn,1);//打印10和2
obj.method(fn,1,2,3);//打印10和4

解析:

  1. fn()前面没有引导对象,是函数调用模式, this是全局对象,输出 10;
  2. arguments0,arguments是一个伪数组对象, 这里调用相当于通过数组的索引来调用。所以,执行时,this就是指arguments,由于传了两个参数,所以 输出为arguments.length就是2。

这里引导对象即宿主就是 arguments对象。

三、构造器调用模式

同样是函数,在单纯的函数模式下,this表示window;在对象方法模式下,this值的是当前对象。除了这两种情况,JavaScript中函数还可以是构造器。将函数作为构造器来使用的语法就是在函数调用前面加上一个new关键字。例如:

//定义一个构造函数
var Person = function(){
    this.name = '程序员';
    this.sayHello = function(){
        console.log('Hello');
    };
};
//调用构造器,创建对象
var p = new Person();
//使用对象
p.sayHello();//Hello

上面的案例首先创建一个构造函数Person,然后使用构造函数创建对象p。这里使用new语法。然后再使用对象调用sayHello()方法。从案例可以看到,此时this指的是对象本身。此外,函数作为构造器还有几个变化,分别为:

  1. 所有需要由对象使用的属性,必须使用this引导;
  2. 函数的return语句意义被改写,如果返回非对象,就返回this。

分享一道面试题 请问顺序执行以下代码,会怎样输出

function Foo(){
    getName = function(){
        console.log(1);
    }
    return this;
}
Foo.getName = function(){
    console.log(2);
}
Foo.prototype.getName = function(){
    console.log(3);
}
var getName = function(){
    console.log(4);
}
function getName(){
    console.log(5);
}

Foo.getName();//输出2.
//调用Foo函数作为对象动态添加的属性方法 getName
//Foo.getName = function(){console.log(2);}

getName();//输出4.
//这里Foo函数还没有执行,getName还没有被覆盖
//所以这里还是最上面的getName=function(){console.log(4);}

Foo().getName();//输出1
//Foo()执行,先覆盖全局的getName,再返回this
//this是window,Foo().getName()就是调用window.getName
//此时全局的getName已被覆盖成function(){console.log(1);}
//所以输出为1
//从这里开始window.getName已被覆盖为function(){console.log(1);}

getName();//输出1
//window.getName(),输出1

new Foo.getName();//输出2
//new 就是找构造函数(),由构造函数结合性,这里即使Foo无参,也不能省略(),所以不是Foo().getName()
//所以Foo.getName为一个整体,等价于new (Foo.getName)();
//而 Foo.getName其实就是函数function(){console.log(2);}的引用
//那么new (Foo.getName)(),就是在以Foo.getName为构造函数,实例化对象。
//就类似于 new Person();Person是一个构造函数


new Foo().getName();//输出3
//new就是找构造函数(),等价于(new Foo() ).getName();
//执行new Foo() => 以Foo为构造函数,实例化一个对象
//(new Foo() ).getName;访问这个实例化对象的getName属性
//实例对象自己并没有getName属性,构造的时候也没有添加,找不到,就到原型中找
//发现Foo.prototype.getName = fucntion(){console.log(3);}
//原型中有,找到了,所以执行(new Foo() ).getName()结果为3

new new Foo().getName();//输出为3
//new就是找构造函数(),等价于new ( (new Foo() ).getName ) ()
//先看里面的(new Foo() ).getName
//new Foo() 以Foo为构造函数,实例化对象
//new Foo().getName 找实例对象的 getName 属性,自己没有,就去原型中找
//发现 Foo.prototype.getName = function() {console.log(3);}
//所以里层(new Foo() ).getName就是以Foo为构造函数实例出的对象的一个原型属性
//属性值为一个函数function(){console.log(3);}的引用
//所以外层new ( (new Foo() ).getName )()在以函数function(){console.log(3);}为构造函数,构造实例
//构造过程中执行了console.log(3),输出3

构造器中的this

分析创建对象的过程,理解this的意义。

var Person = function(){
    this.name = '程序员';
};
var p = new Person(); 

这里首先定义了函数Person,下面分析一下整个执行:

  1. 程序在执行到这一句的时候,不会执行函数体,因此JavaScript的解释器并不知道这个函数的内容。
  2. 接下来执行new关键字,创建对象,解释器开辟内存,得到对象的引用,将新对象的引用交给函数。
  3. 紧接着执行函数,将传过来的对象引用交给this。也就是说,在构造方法中,this就是刚刚被new创建出来的对象。
  4. 然后为this添加成员,也就是为对象添加成员。
  5. 最后函数结束,返回this,将this交给左边的变量。

分析过构造函数的执行以后,可以得到,构造函数中的this就是当前对象。

构造器中的return

在构造函数中return的意义发生了变化,首先如果在构造函数中,如果返回的是一个对象,那么就保留原意。如果返回的是非对相,比如数字、布尔值和字符串,那么就返回this,如果没有return语句,那么也返回this,例如:

//返回一个对象的return
var foo = function(){
    this.name = '张三';
    return {
        name:'李四'
    };
};
//创建对象
var p = new foo();
//访问name属性
console.log(p.name);//李四

执行代码,这里输出的结果是“李四”。因为构造方法中返回的是一个对象,那么保留return的意义,返回内容为return后面的对象,再看如下代码:

//定义返回非对象数据的构造器
var foo = fucntion() {
    this.name = '张三';
    return '李四';
}
//创建对象
var p = new foo();
console.log(p.name);//张三

执行代码,这里输出结果为“张三”,因为这里return的是一个字符串,属于基本类型,那么这里的return语句无效,返回的是this对象。

四、上下文调用模式

就是环境调用模式 => 在不同环境下的不同调用模式 简单说就是统一一种格式,可以实现函数模式与方法模式 语法

  • call形式,函数名.call(...)
  • apply形式,函数名.apply(...)

这两种形式功能完全一样,唯一不同的是参数的形式。先学习apply,再来看call形式

apply方法调用形式

存在上下文调用的目的就是为了实现方法借用,且不会污染对象。

  • 如果需要让函数以函数的形式调用,可以使用 foo.apply(null);//上下文为window

  • 如果希望它是方法调用模式,注意需要提供一个宿主对象 foo.apply(obj);//上下文为传入的obj对象

function foo(num1,num2){
    console.log(this);
    return num1+num2;
}
//函数调用模式
var res1 = foo(123,567);
//方法调用
var o = { name: 'chenshsh' };
o.func = foo;
var res2 = o.func(123,567);

使用apply进行调用,如果函数是带有参数的。apply的第一个参数要么是null要么是对象。

  1. 如果是null,就是函数调用
  2. 如果是对象就是方法调用,该对象就是宿主对象,后面紧跟一个数组参数,将函数所有的参数依次放在数组中
//函数模式
foo(123,567);
foo.apply(null,[123,567]);//以window为上下文执行apply

//方法模式
o.func(123,567);
var o = { name:'chenshsh' };
foo.apply(o,[123,567]);//以o为上下文执行apply

call方法调用

在使用apply调用的时候,函数参数必须以数组的形式存在。但是有些时候数组封装比较复杂,所以引入call调用,call调用与apply完全相同,唯一不同是参数不需要使用数组。

foo(123,456);
foo.apply(null,[123,456]);
foo.call(null,123,456);
  1. 函数调用:函数名.call(null,参数1,参数2,参数3...);
  2. 方法调用:函数名.call(obj,参数1,参数2,参数3...);

不传递参数时,apply和call完全一样