浅析JavaScript中Function对象(二) 之 详解call&apply

  函数是js中最复杂的一块内容,其中call() 和 apply()又是重灾区,初学者往往在这个坑里栽倒,这次来分析这2个函数对象的成员

  一、函数的角色

  在js的体系下,js有3种角色。分别是普通函数、构造器、对象。

  1.普通函数

<script type="text/javascript">
    function f1(){
        console.log(‘这是个函数‘);
    }
</script>

  这里声明的f1,它的角色就是个普通函数

  2.构造器

<script type="text/javascript">
    function Person(name, age){
        this.name = name;
        this.age = age;
    }
    var per = new Person(‘james‘, 28);
</script>

  这里声明的Person,虽然也是函数,但是在本例中它的角色就是构造器。

  3.对象

  

<script type="text/javascript">
    var f2 = function(){
        console.log(‘这个匿名函数是被当对象使用的‘);
    }   f2();
</script>

  这里的f2是引用类型变量,它指向的是一个函数对象,我们也直接称之为函数对象。

  二、this之争

  先来看一个例子

<script type="text/javascript">
    function f1(){
        console.log(this);
    }
    f1();
</script>

  这个代码执行完之后会输出什么?

  答案是:window,那么也就是说,在这个函数中,this是指向window对象的。

  为什么呢?原因是,根据我们之前说过的作用域和作用域链的理论,这个f1是在全局作用域下声明的,那么,只要是在全局作用域下声明的变量,对象,函数,通通都被当成window对象的成员。换句话说,这里声明的f1函数,在调用的时候完全可以这么调用: window.f1(); 。我们一般在写程序的时候,往往为了代码简洁,就把window给省了。这一点跟我们直接使用document对象是一个道理。

  下面我给上例简单做个变形:

<script type="text/javascript">
    function f1(){
        "use strict";//使用js严格模式
        console.log(this);
    }
    f1();//输出undefined
</script>

  这次程序运行的结果是:undefined。什么意思?意思就是,这次运行之后,f1中this是undefined,而不是上例中的window了。

  我们说了,谁是函数的调用者,那么函数中的this就是指向谁,在正常模式下,直接调用f1()和通过window.f1()调用是完全等价的,所以正常模式下看到f1()执行时,它内部的this是指向window对象的。而当我们使用严格模式时,f1()调用时,函数名前面是空的,也就被当作”无主之函数“来调用了。就是没人调用你,那this自然就是undefined。

  两者的区别就是,这次在f1中多了一个“use strict";这样一条语句,这条语句是声明,使用js严格模式来解析代码。

  那么在这里就简单说下严格模式这个概念。

  js有两种运行模式,分别是正常模式和严格模式。我们平时使用的都是正常模式。如果在代码前加一条“use strict";语句,那么其后的代码将以严格模式执行。  

  设立"严格模式"的目的,主要有以下几个:

  - 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;

  

  - 消除代码运行的一些不安全之处,保证代码运行的安全;

  

  - 提高编译器效率,增加运行速度;

  

  - 为未来新版本的Javascript做好铺垫。

  

  我们从以上代码中,可以得出一个结论就是,在一个函数中,this这个关键字的指向是可以改变的。

  

  三、调用函数的另一种形式

  

  函数声明了以后,可以像传统方法一样调用,比如f1();但也可以使用另一种途径来调用。比如:

<script type="text/javascript">
    function f1(){
        console.log(this);
    }
    f1();//输出window
    f1.apply();//输出window
    f1.call();//输出window
</script>

  这里我们连续3次调用f1函数。第一次是普通的函数调用,第2次和第3次是把f1当对象来使用的,然后调用了f1这个对象的apply方法和call方法。并且我们看到的输出结果都是一致的。

  那么问题来了,这个apply()和call()从何而来?

  简单,根据原型和原型链理论,如果自己没有定义,那么就一定是从原型链上拿过来的。我们知道所有的函数其实都是内置对象Function的实例,Function的原型对象,也就是Function.prototype上定义了apply()和call(),所以我们自定义的任何函数,都可以通过函数名打点的形式访问到这2个成员。当然此时是把函数以对象的角色来使用的。

  其实,apply和call还能传递参数。我们先来个试试  

<script type="text/javascript">   ‘use strict‘;
    function f1(){
        console.log(this);
    }
    f1();//输出undefined
    f1.apply();//输出undefined
    f1.call();//输出undefined
</script>

  把上边的代码稍作改变,使用严格模式,运行结果仍然为输出undefined,这跟上一个小节说的是一回事。

  从上边来看,3次调用结果完全相同,我们可以得出一个重要结论:

  call和apply就是用来执行这个函数的。(简直是废话)从这两个单词的字母意思来看也是如此,call就是呼叫,调用的意思,apply也是应用,使用的意思。

  那为什么要使用apply和call呢?这两个成员的存在的意义何在?

  四、call和apply的基本用法

  前边讨论的都是不需要传递参数的函数。下面我们来考察需要传递参数的情况。然后再指出call和apply的真实用途。看下面的例子:  

<script type="text/javascript">
    function add(a, b){
        console.log( (a + b) + " : " + this);
    }
    add(2,3);
</script>

  这里的add函数需要2个参数,正常调用我们都非常熟悉。如果想借助apply和call来调用,那么就必须做出改变,如果仅仅是

add.call(2,3);
add.apply(2,3);

  这样调用会报错。

  call和apply的函数签名是这样的:

  call( thisArg [, arg1, arg2, arg3,.....]);

  apply(thisArg [ [arg1, arg2, arg3, ...]]);

  说明一下:

  1.这两个函数的作用完全相同,就是在如果需要传递参数时,参数的形式有点区别。

  2.两者第1个参数都是thisArg,顾名思义,要求你传一个对象,表示当函数调用时,要把这个对象当成函数内部的this所指对象。这一点很重要,我们后边会详细介绍

  3.如果调用的函数本身需要传递参数,那么这些参数要放在call和apply实参的第2个位置开始传递。并且第一个thisArg必须传递,实在不需要传递,也可以给null。

  4.如果被调用的函数就没有参数,那么通过call和apply来调用函数时,可以给thisArg传参,也可以不传,不传就是默认原始的this指向。

  看了这么多都要懵了,我们看例子,然后来一一对照着说明。还是上边的函数

<script type="text/javascript">
    function add(a, b){
        console.log( (a + b) + " : " + this);
    }
    add.call(null,2,3);
    add.apply(null,[2,3]);
</script>

  输出结果是:,从结果我们可以分析出的结论是:

  1.我们传递thisArg是null,但是并没有改变add函数内部的this指向,这时add函数内部,this指向仍然是window对象。

  2.call传递参数,需要给离散的,单个的值,如果需要传多个,那么需要把多个值用逗号隔开。

  3.apply传递参数,需要给数组,这里的[2,3],就是个数组。 这也是apply和call的唯一差别。其实功能是完全一样的。

  接下里,我们再变一变。

<script type="text/javascript">
    function add(a, b){
        console.log( (a + b) + " : " + this);
    }
    var obj = {name:"james", age:18};
    add.call(obj,2,3);
    add.apply(obj,[2,3]);
</script>

  这次我们声明了一个obj对象,然后让这个对象当做call和apply的thisArg参数传递过去。运行结果是:

  

  我们可以看到这次add函数运行时this指向变了。变成了一个Object对象,这个Object对象就是我们传递过去的obj。现在大家应该清楚call和apply的第一个参数thisArg是干什么用了吧。

  现在我们可以指出call()和apply()的定义:执行函数体,并试图改变(篡改)函数体中this关键字的指向。

  如果还不明白,我们再看一个例子:  

<script type="text/javascript">
    function Person(name, age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.sayHi = function(){
        console.log("你好:" + this.name);
    }
    var per = new Person(‘老李‘, 28);
    per.sayHi();// 输出 “你好:老李"

    function Student(name, age){
        this.name = name;
        this.age = age;
    }
    var stu = new Student(‘老王‘, 18);
    per.sayHi.call(stu);//输出“你好:老王”
    per.sayHi.apply(stu);//输出“你好:老王”
</script>

  代码的注释已经说明了程序执行结果。我们只解释下。本来per调用sayHi(),是要输出this.name。正常调用时,this就是指向由Person这个构造函数实例化出来的这个per对象它自己,所以输出的就是per自己的name属性值:老李。

  当使用call和apply时,我们给他传递的thisArg是stu对象,那么这个时候,在执行sayHi方法时,当运行到语句:console.log("你好:" + this.name)时,这个this就被替换成了stu对象,那么this.name当然也就是读取出了:老李 这个值。

  从这个案例,我们可以得出以下2个结论:

  1.如果call和apply拿来使用,那么十有八九就是用来改变函数内部this指向的。否则你直接正常调用好了,还整什么call和apply?多此一举

  2.Student本身没有声明sayHi(),那么正常情况下,老李这个stu对象,正常情况下是不能打招呼的,但这里我们确实达到了让老李这个stu对象打招呼的目的。我们把这种用法称之为js方法借用。

  怎么理解这个借用呢?首先借用还是要忠实的执行原来声明时定义的代码,只不过是替换掉this这个”主体“而已。打个比方,就相当于你有一张加油卡,你拿加油卡是给你的汽车加油,我没有加油卡,我想加油怎么办呢?我可以借你卡来一用,我借过来了,加油就是给我的车加油。当然不管是改变不改变this,都不能改变函数执行的代码逻辑,你拿了卡只能加油,我借过来卡也只能执行加油的操作,不可能拿了加油卡去银行取钱。

  五、实际用途

  call和apply的实际用途有2种:

  1.方法借用

  2.继承(其实也是借用)

  继承的问题我们放在其他帖子中讨论,这里只给出2个方法借用的例子。

  例1:求数组的最大值

  方法1.自己写个函数,使用for循环遍历这个数组,这个方法谁都会,不赘述。

  方法2.借用Math.max()。这个Math是系统内置对象,它给我们提供了很多数学运算的方法,比如Math.max()就是求一组数的最大值。但是这个max函数不支持传递数组,只支持传递单个单个的离散数据。所以,如果想传递一个数组给它,会报错的。好了,虽然不能直接调用,但是我们可以借用。  

<script type="text/javascript">
    var ary = [22, 334, 33, 21, 83];
    var max = Math.max.apply(null, ary);
    console.log(max);
</script>

  这里需要解释下为什么这么调用,为什么用apply,又为什么要给他传一个null。

  1.Math.max()的签名是:Math.max(arg1, arg2, arg3...);

  2.我们这里不需要改变this值,只需要通过方法借用,把数组ary里的各个项,当做max函数要的离散的参数传给它,借用一下即可。

  所以,我们给apply方法传递的thisArg参数为null,因为我们无意改变max函数在执行时的this指向。再多说一句,由于max函数在内部执行的时候,根本就没使用到this这个关键字,所以,这个时候你给thisArg传什么都无所谓,包括把ary自己传过去也行。如下: var max = Math.max.apply(ary, ary); 。

  为什么选用apply而不用call,因为我们这ary是一个数组,apply接受的参数正好是个数组,而call只接收离散的单个单个的参数,所以本例中只能选用apply来实现借用。

  例2.把伪数组转化为数组。

  方法1.自己写一个函数,通过for循环遍历搞定。

  方法2.借用Array对象的slice()函数  

<script type="text/javascript">
    var wsz = {0:‘james‘, 1:‘terry‘, 2:‘jerry‘, 3:‘tod‘, length:4};
    var ary1 = Array.prototype.slice.call(wsz);
    var ary2 = Array.prototype.slice.apply(wsz);
    console.log(ary1);
    console.log(ary2);
</script>

  输出是:

  这个例子中,使用apply和call是一样的效果。下面来做解释说明:

  1.slice()函数作用是从目标函数中截取连续的一部分元素形成一个新的数组,并返回这个新数组。其签名是:Array.prototype.slice(start, end)。如果只传1个参数,将以这个参数为起始点,开始截取目标数组中的元素直到末尾;如果不传参数,返回的新数组将与老数组完全相同

  2.我们现在的例子,就是想让伪数组wsz中每一个键值对(length除外)都转化成数组的一个项,而不是某一部分,所以我们不需要给slice传递start参数和end参数。所以我们只需要传递给call或者apply那个thisArg参数。我们这里必须把待转化的对象wsz作为thisArg,因为在slice方法内部,是通过this这个关键字来遍历数组成员的。所以这时候要借用slice,就必须让slice函数的代码在执行时,this必须指向当前待转化的对象wsz。所以这里,我们传递给call和apply的参数wsz,是传递给thisArg的。

  3.如果你真的是要把伪数组wsz中的某几个连续的成员转化成数组,那么就必须传start和end参数了。形如:

var ary1 = Array.prototype.slice.call(wsz, 1, 3);
var ary2 = Array.prototype.slice.apply(wsz, [1, 3]);

  这里我们应该看到,call传递的是单个的离散的值,apply就要传递一个数组。

  4.其实还可以这么写 var ary1 = [].slice.call(wsz, 1, 3); 其中[]就是一个空数组对象。由于slice方法是定义在Array的原型对象中的,所以,所有的数组实例对象都能通过原型链访问到slice方法,而且这种写法更简单。我们在开发中往往使用这种更简化的写法。大家要看得懂。

  好了,到这里我就把call和apply的来龙去脉说清楚了。

原文地址:https://www.cnblogs.com/ldq678/p/9711494.html

时间: 2024-10-07 23:36:57

浅析JavaScript中Function对象(二) 之 详解call&apply的相关文章

浅析JavaScript中Function对象(一)

一.Function对象及其原型对象 Function对象是js中一个非常重要的对象,所有通过function关键字声明的函数,本质上都是由Function这个特殊的构造器对象创建出来的,也就是new出来的. 首先要明确的一点就是,在JS中万物皆对象,所以函数本身也是对象,只不过函数对象比较特殊,比其他对象多一个prototype属性.所以Function既是对象,也是函数(构造器),其实更准确的说就是函数对象. 其次,更特殊的一点是,由于js中所有的对象都是由构造器new出来的,那么Funct

全面理解Javascript中Function对象的属性和方法

函数是 JavaScript 中的基本数据类型,在函数这个对象上定义了一些属性和方法,下面我们逐一来介绍这些属性和方法,这对于理解Javascript的继承机制具有一定的帮助. 属性(Properties) arguments 获取当前正在执行的 Function 对象的所有参数,是一个类似数组但不是数组的对象,说它类似数组是因为其具有数组一样的访问性质及方式,可以由arguments[n]来访问对应的单个参数的值,并拥有数组长度属性length.还有就是arguments对象存储的是实际传递给

javascript 中关于call方法的详解。

关于javascript中的call方法,网上查了一些资料总是不得详解.总结网上的观点,call有两个妙用: 1: 继承.(不太喜欢这种继承方式.) 2: 修改函数运行时的this指针. js中关于call的解释如下: js关于call的这份文档容易让人迷糊.而<javascript权威指南>对call的描述就比较容易理解了. 注意红色框中的部分,f.call(o)其原理就是先通过 o.m = f 将 f作为o的某个临时属性m存储,然后执行m,执行完毕后将m属性删除. 如 function f

JavaScript中继承的实现方法--详解

最近看<JavaScript王者归来>中关于实现继承的方法,做了一些小总结: JavaScript中要实现继承,其实就是实现三层含义:1.子类的实例可以共享父类的方法:2.子类可以覆盖父类的方法或者扩展新的方法:3.子类和父类都是子类实例的“类型”. JavaScript中,并不直接从语法上支持继承,但是可以通过模拟的方法来实现继承,以下是关于实现继承的几种方法的总结:1.构造继承法2.原型继承法3.实例继承法4.拷贝继承法 1.构造继承法:在子类中执行父类的构造函数. 1<SCRIPT

JavaScript中事件委托(事件代理)详解

在JavaScript的事件中,存在事件委托(事件代理),那么什么是事件委托呢? 事件委托在生活中的例子: 有三个同事预计会在周一收到快递.为签收快递,有两种办法:一是三个人在公司门口等快递:二是委托给前台MM代为签收.现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递).前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款.这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收. 事

JavaScript中var关键字的使用详解

作用 声明作用:如声明个变量. 语法 ? 1 var c = 1; 省略var 在javascript中,若省略var关键字而直接赋值,那么这个变量为全局变量,哪怕是在function里定义的. ? 1 2 3 4 5 6 7 8 <script type="text/javascript">   function Define() {     a = 2;   }   function Hello() {     alert(a);   } </script>

JavaScript中的面向对象编程,详解原型对象及prototype,constructor,proto,内含面向对象编程详细案例(烟花案例)

面向对象编程: 面向:以什么为主,基于什么模式 对象:由键值对组成,可以用来描述事物,存储数据的一种数据格式 编程:使用代码解决需求 面向过程编程: 按照我们分析好的步骤,按步骤解决问题 优点:性能比面向对象高,适合跟硬件联系很紧密的东西 缺点:没有面向对象那么容易维护,复用,扩展 面向对象编程: 把事务分解成一个个对象,然后由对象之间分工与合作,分工明确,每一个对象都是功能中心 面向对象特性:封装性.继承性 .多态性 封装性:将一个功能封装起来,小封装 将多个函数封装起来,整体封装起来形成一个

javascript中set与get方法详解

其中get与set的使用方法: 1.get与set是方法,因为是方法,所以可以进行判断. 2.get是得到 一般是要返回的   set 是设置 不用返回 3.如果调用对象内部的属性约定的命名方式是_age    然后就是几个例子来简单说明一下: var person ={ _name : "chen", age:21, set name(name) {this._name = name;},get name() {return this._name;}}console.log(pers

javascript中window.event事件用法详解

转自http://www.jb51.net/article/32564.htm描述 event代表事件的状态,例如触发event对象的元素.鼠标的位置及状态.按下的键等等. event对象只在事件发生的过程中才有效. event的某些属性只对特定的事件有意义.比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义. 例子下面的例子检查鼠标是否在链接上单击,并且,如果shift键被按下,就取消链接的跳转. 复制代码代码如下: <