ECAMScript 3给Function的原型定义了两个方法,它们是 `Function.prototype.call` 和 `Function. prototype.apply`。在实际开发中,特别是在一些函数式风格的代码编写中,call 和 apply 方法尤为有用。在 JavaScript 版本的设计模式中,这两个方法的应用也非常广泛,能熟练运用这两个方 法,是我们真正成为一名 JavaScript 程序员的重要一步。

1.call和apply的区别

Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它们的作用一模 一样,区别仅在于传入参数形式的不同。

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下 标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传 递给被调用的函数:

var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );

在这段代码中,参数 1、2、3 被放在数组中一起传入 func 函数,它们分别对应 func 参数列 表中的 a、b、c。

call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向, 从第二个参数开始往后,每个参数被依次传入函数:

var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );

当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的 区别,JavaScript 的参数在内部就是用一个数组来表示的。从这个意义上说,apply 比 call 的使用 率更高,我们不必关心具体有多少参数被传入函数,只要用 apply 一股脑地推过去就可以了。

call 是包装在 apply 上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想 一目了然地表达形参和实参的对应关系,那么也可以用 call 来传送参数。

当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null,函数体内的 this 会指 向默认的宿主对象,在浏览器中则是 window:

var func = function( a, b, c ){
alert ( this === window ); // 输出 true
};
func.apply( null, [ 1, 2, 3 ] );

但如果是在严格模式下,函数体内的 this 还是为 null:

var func = function( a, b, c ){
"use strict";
alert ( this === null ); // 输出 true
}
func.apply( null, [ 1, 2, 3 ] );

有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其 他对象的方法。那么我们可以传入 null 来代替某个具体的对象:

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5 

2.call和apply的用途

2.1  改变 this 指向

call 和 apply 最常见的用途是改变函数内部的 this 指向,我们来看个例子:

var obj1 = {
name: 'sven'
};
var obj2 = {
name: 'anne'
};
window.name = 'window';
var getName = function(){
alert ( this.name );
};
getName(); // 输出: window
getName.call( obj1 ); // 输出: sven
getName.call( obj2 ); // 输出: anne

当执行 getName.call( obj1 )这句代码时,getName 函数体内的 this 就指向 obj1 对象,所以 此处的

var getName = function(){
alert ( this.name );
};

实际上相当于:

var getName = function(){
alert ( obj1.name ); // 输出: sven
};

在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点,div 节点 的 onclick 事件中的 this 本来是指向这个 div 的:

document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
};

假如该事件函数中有一个内部函数 func,在事件内部调用 func 函数时,func 函数体内的 this 就指向了 window,而不是我们预期的 div,见如下代码:

document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
var func = function(){
alert ( this.id ); // 输出:undefined
}
func();
};

这时候我们用 call 来修正 func 函数内的 this,使其依然指向 div:

document.getElementById( 'div1' ).onclick = function(){
var func = function(){
alert ( this.id ); // 输出:div1
}
func.call( this );
};

使用 call 来修正 this 的场景,比如修正 document.getElementById 函数内部“丢失”的 this,代码如下:

document.getElementById = (function( func ){
return function(){
return func.apply( document, arguments );
}
})( document.getElementById );
var getId = document.getElementById;
var div = getId( 'div1' );
alert ( div.id ); // 输出: div1

2.2 Function.prototype.bind

大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this指向, 即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:

Function.prototype.bind = function( context ){
var self = this; // 保存原函数
return function(){ // 返回一个新的函数
return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context
// 当作新函数体内的 this
}
};
var obj = {
name: 'sven'
};
var func = function(){
alert ( this.name ); // 输出:sven
}.bind( obj);
func();

我们通过 Function.prototype.bind 来“包装”func 函数,并且传入一个对象 context 当作参数,这个 context 对象就是我们想修正的 this 对象。

在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一 个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新 函数内部,self.apply( context, arguments )这句代码才是执行原来的 func 函数,并且指定 context 对象为 func 函数体内的 this。

这是一个简化版的 Function.prototype.bind 实现,通常我们还会把它实现得稍微复杂一点, 使得可以往 func 函数中预先填入一些参数:

Function.prototype.bind = function(){
var self = this, // 保存原函数
context = [].shift.call( arguments ), // 需要绑定的 this 上下文
args = [].slice.call( arguments ); // 剩余的参数转成数组
return function(){ // 返回一个新的函数
return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) );
// 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
// 并且组合两次分别传入的参数,作为新函数的参数
}
};
var obj = {
name: 'sven'
};
var func = function( a, b, c, d ){
alert ( this.name ); // 输出:sven
alert ( [ a, b, c, d ] ) // 输出:[ 1, 2, 3, 4 ]
}.bind( obj, 1, 2 );
func( 3, 4 );

2.3 借用其他对象的方法

我们知道,杜鹃既不会筑巢,也不会孵雏,而是把自己的蛋寄托给云雀等其他鸟类,让它们 代为孵化和养育。同样,在 JavaScript 中也存在类似的借用现象。

借用方法的第一种场景是“借用构造函数”,通过这种技术,可以实现一些类似继承的效果:

var A = function( name ){
this.name = name;
};
var B = function(){
A.apply( this, arguments );
};
B.prototype.getName = function(){
return this.name;
};
var b = new B( 'sven' );
console.log( b.getName() ); // 输出: 'sven'

借用方法的第二种运用场景跟我们的关系更加密切。

函数的参数列表 arguments 是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。这种情况下,我们常常会借用 Array.prototype 对象上的方法。比如想往 arguments 中添加一个新的元素,通常会借用 Array.prototype.push:

(function(){
Array.prototype.push.call( arguments, 3 );
console.log ( arguments ); // 输出[1,2,3]
})( 1, 2 );

在操作 arguments 的时候,我们经常非常频繁地找 Array.prototype 对象借用方法。

想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去 arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。那么这种机制的内部实现原理是什么呢?我们不妨翻开 V8 的引擎源码,以 Array.prototype.push 为例,看看 V8 引 擎中的具体实现:

function ArrayPush() {
var n = TO_UINT32( this.length ); // 被 push 的对象的 length
var m = %_ArgumentsLength(); // push 的参数个数
for (var i = 0; i < m; i++) {
this[ i + n ] = %_Arguments( i ); // 复制元素 (1)
}
this.length = n + m; // 修正 length 属性的值 (2)
return this.length;
};

通过这段代码可以看到,Array.prototype.push 实际上是一个属性复制的过程,把参数按照 下标依次添加到被 push 的对象上面,顺便修改了这个对象的 length 属性。至于被修改的对象是谁,到底是数组还是类数组对象,这一点并不重要。

由此可以推断,我们可以把“任意”对象传入 Array.prototype.push:

var a = {};
Array.prototype.push.call( a, 'first' );
alert ( a.length ); // 输出:1
alert ( a[ 0 ] ); // first

这段代码在绝大部分浏览器里都能顺利执行,但由于引擎的内部实现存在差异,如果在低版 本的 IE 浏览器中执行,必须显式地给对象 a 设置 length 属性:

var a = {
length: 0
};

前面我们之所以把“任意”两字加了双引号,是因为可以借用 Array.prototype.push 方法的对象还要满足以下两个条件,从 ArrayPush 函数的(1)处和(2)处也可以猜到,这个对象至少还要满足:

  • 对象本身要可以存取属性
  • 对象的 length 属性可读写

对于第一个条件,对象本身存取属性并没有问题,但如果借用 Array.prototype.push 方法的不是一个 object 类型的数据,而是一个 number 类型的数据呢? 我们无法在 number 身上存取其他数据,那么从下面的测试代码可以发现,一个 number 类型的数据不可能借用到 Array.prototype. push 方法:

var a = 1;
Array.prototype.push.call( a, 'first' );
alert ( a.length ); // 输出:undefined
alert ( a[ 0 ] ); // 输出:undefined

对于第二个条件,函数的 length 属性就是一个只读的属性,表示形参的个数,我们尝试把 一个函数当作 this 传入 Array.prototype.push:

var func = function(){};
Array.prototype.push.call( func, 'first' );
alert ( func.length );
// 报错:cannot assign to read only property ‘length’ of function(){}

call 和 apply方法解析的更多相关文章

  1. javascript中apply()方法解析-简单易懂!

    今天看到了js的call与apply的异同,想着整理一下知识点,发现了一篇好文章,分享过来给大家,写的非常好! 参考: http://www.cnblogs.com/delin/archive/201 ...

  2. call(),apply()方法解析(一)

    1.call()和apply()的作用是改变this指向,区别是传参列表不同(前者连续参数,后者为参数数组) 2.方法定义: function.apply(thisObj[, argArray]) f ...

  3. js方法call和apply实例解析

    在js编程中实现继承时 用到了两个很特殊的方法,call和apply. 在ECMAScript v3中,给Function原型定义了这两个方法,这两个方法的作用都是一样的:使用这两个方法可以像调用其他 ...

  4. 详细解析arry.map() ,function.apply() 方法

    首先转载一篇博文:关于map 和callbackfn 的一些参数和返回值可以查看以下链接. http://www.cnblogs.com/xuan52rock/p/4460938.html array ...

  5. Js apply方法详解

    我在一开始看到javascript的函数apply和call时,非常的模糊,看也看不懂,最近在网上看到一些文章对apply方法和call的一些示例,总算是看的有点眉目了,在这里我做如下笔记,希望和大家 ...

  6. js巧用apply方法实现数组最值以及合并

    尽管js的apply方法在平常的使用中并不多见,但是在某些地方使用的还是很有帮助性的,这里就和大家说两个比较实用的例子:1.数组最大最小值 求数组中的最大最小值,js有相应的方法:Math.min() ...

  7. Js apply 方法 详解

    Js apply方法详解 我在一开始看到JavaScript的函数apply和call时,非常的模糊,看也看不懂,最近在网上看到一些文章对apply方法和call的一些示例,总算是看的有点眉目了,在这 ...

  8. 深入学习JavaScript: apply 方法 详解(转)——非常好

    主要我是要解决一下几个问题: 1.        apply和call的区别在哪里 2.        什么情况下用apply,什么情况下用call 3.        apply的其他巧妙用法(一般 ...

  9. Js apply 方法 具体解释

    Js apply方法具体解释 我在一開始看到javascript的函数apply和call时,很的模糊,看也看不懂,近期在网上看到一些文章对apply方法和call的一些演示样例,总算是看的有点眉目了 ...

随机推荐

  1. 11.全局变量(static)

    1.数组 数组名是常量 2. 指针数组 4.局部变量 (1).作用域 作用的范围: (2).普通局部变量 在{}内定义: 只有执行到定义变量的这个语句,系统才会给这个变量分配空间. 当离开{},这个非 ...

  2. php json 中文不转义 & 转义为中文

    JSON_UNESCAPED_UNICODE private function decodeUnicode($str){ return preg_replace_callback('/\\\\u([0 ...

  3. Django---框架简介和工程搭建

    Django框架 一.Django介绍 二.Django工程搭建 回到顶部 一.Django介绍 1.简介    Django的主要目的是简便.快速的开发数据库驱动的网站.它强调代码复用,多个组件可以 ...

  4. vi检索

    / :检索 n    :向下执行上一步的检索 N   :向上执行上一步的检索

  5. html-webpack-plugin插件使用

    项目使用hightopo框架,使用webpack打包.这里的场景是:点击预览按钮,页面会打开一个新页面. 但是由于使用了webpack打包,所以直接使用以下代码是不行的.报404 window.ope ...

  6. 使用python写的一个代码统计程序

    # encoding="utf-8" """ 统计代码行数 """ import sys import os def c ...

  7. VS2017 + EF6连接MySql

    VS2017 + EF6连接MySql   原地址:https://blog.csdn.net/mzhifa/article/details/80999105 VS2017 + EF6连接MySql ...

  8. java中xxe漏洞修复方法

    java中禁止外部实体引用的设置方法不止一种,这样就导致有些开发者修复的时候采用的错误的方法 之所以写这篇文章是有原因的!最早是有朋友在群里发了如下一个pdf, 而当时已经是2019年1月末了,应该不 ...

  9. Python语言——Python语言概述

    Python语言概述 计算机语言概述 语言:交流工具,沟通媒介 计算机语言:人和计算机交流的工具,翻译官 Python语言简述 Python是计算机语言的一种 Python编程语言: 代码:人类语言, ...

  10. Liunx系统升级自带的Python版本

    一.查看系统信息 [root@localhost ~]# cat /etc/redhat-release CentOS release 6.4 (Final) [root@localhost ~]# ...