前言

在学习javascript函数的时候,有几个经常很容易混淆的方法,call,apply,bind,caller,callee,这些方法的使用,在此通过查阅相关书籍和资料,整理了一篇博客,本文将详细介绍call,apply,bind,caller,callee这些方法的使用,如果有讲解有歧义的地方,还请大家海涵。可以多多和我交流,在下方留下您宝贵的评论。

Function函数

在学习call,apply,bind,caller,callee这些方法之前,我们需要先了解函数到底是什么东西,毕竟这些方法都是围绕函数进行展开的。

函数的定义:

Function 构造函数 创建一个新的Function对象,在 JavaScript 中,每个函数实际上都是一个Function对象,使用Function构造器生成的Function对象是在函数创建时解析的。这比你使用函数声明或者函数表达式(function)并在你的代码中调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。

  • 所有被传递到构造函数中的参数,都将被视为将被创建的函数的参数,并且是相同的标示符名称和传递顺序
  • javascript中的函数就是对象,对象就是“键/值”对的集合并拥有一个连接到原型对隐藏连接
  • 每个函数都是Function类型的实例,而且都与其它引用类型一样具有属性和方法
  • 由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定

函数的定义有两种方法,第一种是函数声明,第二种是函数表达式

函数声明

            console.log(foo(10,10));//
function foo(a,b){
return a+b
}

函数表达式

            console.log(sum(10,10));//TypeError: sum is not a function
var sum=function(a,b){
return a+b;
}

同样是用来表达函数的方式,为什么函数声明和函数表达式的差别那么大呢?且听我慢慢说来,解析器在向执行函数环境中加载数据时,对函数声明和函数表达式并非一视同仁,解析器会率先读取函数声明,并使其在执行任何代码之前可用(也就是我们常说的变量提升),至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正的解析执行。

对代码求值时,javascript引擎在第一遍会声明函数并将它们放到源代码树的顶部,所以,即使声明函数的代码在调用它的代码后面,javascript引擎也能把函数声明提升到顶部。

没有重载(深入理解)

很多编程语言中都有重载这个概念,比如java中的方法重载,构造函数重载等等,但是JavaScript这一门动态语言中偏偏没有方法重载,我们来看下示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>没有重载(深入理解)</title>
</head>
<body>
<script type="text/javascript">
function addSomeNumber(number){
return number+100;
}
function addSomeNumber(num){
return num+200;
}
var result=addSomeNumber(100);
console.log(result);//300
</script>
</body>
</html>

显然,这个例子中声明了两个同名函数,而结果则是后者的函数覆盖了前面的函数,以上代码实际上与下面的代码没有什么区别

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>没有重载(深入理解)</title>
</head>
<body>
<script type="text/javascript">
var addSomeNumber=function(number){
return number+100;
}
addSomeNumber=function(num){
return num+200;
}
var result=addSomeNumber(100);
console.log(result);//300
</script>
</body>
</html>

通过重写之后的代码,很容易看清楚到底是怎么回事,在创建第二个函数时,实际上覆盖了引用第一个函数的变量addSomeNumber。

作为值的函数

因为ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用,也就是说不仅可以像传递参数一样把一个函数传递给另一个函数,而且还可以将一个函数作为另一个函数的结果返回,比如常见的回调函数就是。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>作为值的函数</title>
</head>
<body>
<script type="text/javascript">
function callSomeFunction(someFunction,someArgumet){
return someFunction(someArgumet);
}
function add(num){
return num+10
}
var result1=callSomeFunction(add,10);
console.log(result1);//20
function getGreeting(name){
return 'hello'+name;
}
var result2=callSomeFunction(getGreeting,'一只流浪的kk');
console.log(result2);//hello 一只流浪的kk
</script>
</body>
</html>

callSomeFunction函数接收两个参数,第一个参数是一个函数,第二个参数是要传递给该函数的一个值,当然,可以从一个函数中返回另一个函数,我们看下面的示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>作为值的函数</title>
</head>
<body>
<script type="text/javascript">
function comPare(propertyName){
return function(a,b){
var value1=a[propertyName];
var value2=b[propertyName];
if(value1>value2){
return -1
}else if(value1<value2){
return 1;
}else{
return 0;
}
}
}
var data=[{name:'zhangsan',age:28},{name:'lisi',age:29}];
data.sort(comPare('name'));
console.log(data);//[{name:'lisi',age:29},{name:'zhangsan',age:28}];
data.sort(comPare('age'));
console.log(data);//[{name:'lisi',age:29},{name:'zhangsan',age:28}];
</script>
</body>
</html>

这个函数定义看起来有点复杂,但实际上无非就是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个return操作符,在内部函数接收到propertyName参数后,它会使用方括号表示法取得给定属性的值,取得了想要的属性值之后,定义比较函数就非常简单了。

函数内部属性arguments和this

在函数内部,有两个特殊的对象:arguments和this,其中arguments它是一个类数组,包含着传入函数中的所有参数,this引用的是函数执行的环境对象,或者也可以说是this的值(当在网页的全局作用域中调用函数时,this对象的引用就是window)

arguments

<script type="text/javascript">
function counter(){
var sum=0;
for(var i=0;i<arguments.length;i++){
sum+=arguments[i];
}
return sum;
} console.log(counter(199,991,1,2,3,4,5));//1205
console.log(counter());//0
</script>

这里的arguments是一个隐式对象,不声明也在函数中,内部函数可以访问外部函数的任意内容,但是不能直接访问外部函数的arguments与this对象

        <script type="text/javascript">
function f1(){
console.log(arguments.length);//
f2=function(){
console.log(arguments.length);//
}
return f2;
}
var f=f1(1,2,3);
f();
</script>

this

        <script type="text/javascript">
window.color='red';
var o={
color:'blue'
};
function sayColor(){
console.log(this.color);//red;
}
sayColor();
o.sayColor=sayColor
o.sayColor();//blue
</script>

上面这个函数sayColor()是在全局作用域中定义的,它引用了this对象,由于在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中,引用不同的对象,当在全局作用域中调用sayColor()时,this引用的是全局对象window,换句话说,对this.color求值会转换成window.color求值,于是结果就返回red,而当把这个函数赋给对象o并调用o.sayColor()时,this的引用的是对象o,因此对this.color求值转换成o.color求值,结果返回blue。

注意:函数的名字仅仅是一个包含指针的变量而已。

构造函数

在javascript中对象构造函数可以创建一个对象

<script type="text/javascript">
/*构造函数*/
//可以简单的认为是一个类型的定义
function Student(name,age){
this.name=name;
this.age=age;
this.show=function(){
console.log(this.name+","+this.age);
}
} //通过new关键字调用构造函数,创建一个对象tom
var rose=new Student("rose",18);
var jack=new Student("jack",20); rose.show();//rose,18
jack.show();//jack,20
</script>

学会了函数的相关知识之后,我们就开始学习call(),apply(),caller,callee,bind()的相关用法,一起来看看吧!

函数的属性和方法

callee

当函数被调用时,它的arguments.callee对象就会指向自身,也就是一个对自己的引用,可能描述的不是太清楚,我们通过案例进行讲解,最常见的示例就是递归了,我们看下示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script type="text/javascript">
function factorial(num){
if(num<=1){
return 1;
}else{
return num*factorial(num-1)
}
}
console.log(factorial(5));//120
</script>
</body>
</html>

这个示例中有一个非常明显的弊端,就是这个函数的执行与函数名factorial仅仅耦合在了一起,我们编程讲究的是高内聚,低耦合。为了解决这个问题,我们就会使用arguments.callee来代替函数名。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script type="text/javascript">
function factorial(num){
if(num<=1){
return 1;
}else{
return num*arguments.callee(num-1)
}
}
console.log(factorial(5));//120
</script>
</body>
</html>

这样一来我们就大大的降低了耦合度,无论函数名是什么,怎么执行都不会有影响

caller

这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>caller的使用</title>
</head>
<body>
<script type="text/javascript">
function outer(){
inner();
}
function inner(){
console.log(inner.caller);
}
outer();
</script>
</body>
</html>

结果:

以上代码输出outer()函数的源代码,因为outer()调用了inner(),所以inner.caller就指向outer(),为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>caller的使用</title>
</head>
<body>
<script type="text/javascript">
function outer(){
inner();
}
function inner(){
console.log(arguments.callee.caller);
}
outer();
</script>
</body>
</html>

输出的结果和之前的一样,在这里我们使用arguments.callee代替了inner

caller和this的区别

this是指调用方法的对象,而caller是指调用函数的函数

<script type="text/javascript">
function add(n)
{
console.log("add被调用");
if(n<=2){
return 1;
}
return add.caller(n-1)+add.caller(n-2);
} function calc(n){
console.log("calc被调用");
return add(n);
} //1 1 2
console.log(calc(3));
</script>

总结

  • 每个函数都包含两个非继承而来的方法,apply()和call(),这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内的this对象的值。
  • apply()方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组,第二个参数可以是array示例,也可以是arguments对象。
  • call()方法和apply()方法的作用相同,它们的区别在于接收参数的不同,对于call()而言,第一个参数是this的值没有变化,变化的是其余参数都直接传递给函数,换句话说,在使用call()方法时,传递给函数的参数必须每个列举出来。

call()

Function.call(obj,[param1[,param2[,…[,paramN]]]])
obj:这个对象将代替Function类里this对象
params:这个是一个参数列表

调用一个对象的一个方法,以另一个对象替换当前对象

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>call()方法的使用</title>
</head>
<body>
<script type="text/javascript">
function sum(num1,num2){
return num1+num2;
}
function callSum(num1,num2){
return sum.call(this,num1,num2);
}
console.log(callSum(10,10));//20
</script>
</body>
</html>

在使用call()方法的情况下,callSum()必须明确地传入每一个参数

apply()

Function.apply(obj,args)方法能接收两个参数
obj:这个对象将代替Function类里this对象
args:这个是数组,它将作为参数传给Function(args-->arguments)

注意

  • 如果 argArray 不是一个有效的数组或者不是arguments对象,那么将导致一个 TypeError
  • 如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>apply()方法的使用</title>
</head>
<body>
<script type="text/javascript">
//定义一个人类
function Person(name,age){
this.name=name;
this.age=age;
}
//定义一个学生类
function Student(name,age,grade){
Person.apply(this,arguments);//此时的this指代Studnent
this.grade=grade;
}
//创建一个学生
var mark=new Student('zhagnsan',21,'七年级');
console.log(mark.name,mark.age,mark.grade);//zhangsan,21,七年级
</script>
</body>
</html>

特别奇怪的现象,我们明明没有给name和age属性赋值,为什么又存在这两个属性的值呢?

分析:Person.apply(this,arguments);

this:在创建对象在这个时候代表的是student

arguments:是一个数组,也就是[“zhangsan”,”21”,”一年级”];

也就是通俗一点讲就是:用student去执行Person这个类里面的内容,在Person这个类里面存在this.name等之类的语句,这样就将属性创建到了student对象里面。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>apply()方法的使用</title>
</head>
<body>
<script type="text/javascript">
function sum(num1,num2){
return num1+num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments);
}
function callSum2(num1,num2){
return sum.apply(this,[num1,num2]);
}
console.log(callSum1(10,10));//20
console.log(callSum2(10,10));//20
</script>
</body>
</html>

在上面这个示例中,callSum1()在执行sum()函数时传入了this作为this的值(因为是在全局作用域中调用的,所以传入的值就是window对象)和arguments对象,二callSum2()同样也调用了sum()函数,但它传入的则是this和一个参数数组,这两个函数都会正常执行并返回结果。

注意:在严格模式下,为指定环境对象而调用函数,则this值不会转型为window,除非明确把函数添加到某个对象或者调用apply()或call(),否则this的值是undefined

什么情况下用apply(),什么情况下用call()?

在给对象参数的情况下,如果参数的形式是数组的时候,比如apply示例里面传递了参数arguments,这个参数是数组类型,并且在调用Person的时候参数的列表是对应一致的(也就是Person和Student的参数列表前两位是一致的) 就可以采用 apply , 如果我的Person的参数列表是这样的(age,name),而Student的参数列表是(name,age,grade),这样就可以用call来实现了,也就是直接指定参数列表对应值的位置(Person.call(this,age,name,grade));

事实上,传递参数并非apply()和call()真正的用武之地,它们真正强大的地方是能够扩充函数赖以运行的作用域,看如下示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script type="text/javascript">
window.color='red';
var o={
color:'blue'
}
function sayColor(color){
console.log(this.color)
}
sayColor();//red
sayColor.call(this);//red
sayColor.call(window);//red
sayColor.call(o);//blue
</script>
</body>
</html>

使用call()或者apply()来扩充作用域的最大好处,就是对象与方法不需要任何耦合关系

bind()

这个方法会创建一个函数的实例,其this的值会被绑定到传给bind()函数的值

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>bind方法的使用</title>
</head>
<body>
<script type="text/javascript">
window.color='red';
var o={
color:'blue'
}
function sayColor(color){
console.log(this.color)
}
var objSayColor=sayColor.bind(o);
objSayColor();//blue
</script>
</body>
</html>

在这里,sayColor()调用bind并传入对象o,创建了objSayColor()函数,objSayColor()函数的this的值等于o,因此即使全局作用域中调用这个函数,也会看到blue

length

在声明函数时指定的命名参数的个数

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Function</title>
</head>
<body>
<h2>Function - length</h2>
<script>
function f1(n1,n2)
{
console.log("实际带入的参数个数:"+arguments.length);//1
}
console.log("定义的命名参数个数:"+f1.length);
f1(1);
f1(1,2,3);
</script>
</body>
</html>

apply()方法的妙处

apply在实际应用中有非常之多的妙处,在这里我就补充两点,一种是array种push的短板,二是使用Math.Max(),Math.min()求最大值,最小值等等。

(1)Array.prototype.push 可以实现两个数组合并

我们知道数组的push方法没有提供push一个数组,但是提供了push(param1,param,…paramN) 所以同样也可以通过apply来装换一下这个数组,我们看下示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>apply的应用一</title>
</head>
<body>
<script type="text/javascript">
var arr1=[1,2,3];
var arr2=[4,5,6];
Array.prototype.push.apply(arr1,arr2);
console.log(arr1);//[1,2,3,4,5,6]
</script>
</body>
</html>

也可以这样理解,arr1调用了push方法,参数是通过apply将数组装换为参数列表的集合

(2)Math.max()和Math.min()求数组的最大值和最小值

Math.max(a,b)和Math.min(a,b)只能求两个数中的最大值和最小值,但是我们想要求数组中的最大值和最小值的时候却无法实现,而使用apply方法可以巧妙的实现,如下示例

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>apply的应用二</title>
</head>
<body>
<script type="text/javascript">
var arr=[1,2,3,4,5];
console.log(Math.max.apply(null,arr));//5
console.log(Math.min.apply(null,arr));//1
</script>
</body>
</html>

我们看到巧妙的使用apply可以非常简单的实现Math.max()和Math.min()求最大值和最小值

总结

call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就不一样了,总结如下

  • call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 Function.call(this.Obj,arg0,arg1,...)
  • apply 的所有参数都必须放在一个数组里面传进去 Function.apply(thisObj,[arguments])
  • bind 除了返回是函数以外,它 的参数和 call 一样

当然,三者的参数不限定是 string 类型,允许是各种类型,包括函数 、 object 等等

javascript学习总结之函数的更多相关文章

  1. JavaScript学习03 JS函数

    JavaScript学习03 JS函数 函数就是包裹在花括号中的代码块,前面使用了关键词function: function functionName() { 这里是要执行的代码 } 函数参数 函数的 ...

  2. 廖雪峰 JavaScript 学习笔记(函数)

    JavaScript中,定义函数的方式如下: function abs(x) { if (x >= 0) { return x; } else { return -x; } } 上述abs()函 ...

  3. javascript学习—理解addLoadEvent函数

    onload事件是HTML DOM Event 对象的一个属性,又叫事件句柄(Event Handlers),它会在页面或图像加载完成后(注意是加载完成后)立即发生. window.onload = ...

  4. javascript学习笔记--迭代函数

    概要 这里的迭代函数指的是对数组对象的操作方法,js数组共有五个迭代函数:every.fifter.forEach.map.some. 1.every every方法,返回值为Boolean类型,tr ...

  5. JavaScript学习03(函数)

    函数 函数定义 JavaScript 函数是通过 function 关键词定义的. 声明定义 function functionName(parameters) { 要执行的代码 } 被声明的函数不会 ...

  6. 【JavaScript学习笔记】函数、数组、日期

    一.函数 一个函数应该只返回一种类型的值. 函数中有一个默认的数组变量arguments,存储着传入函数的所有参数. 为了使用函数参数方便,建议给参数起个名字. function fun1(obj, ...

  7. 前端学习 第二弹: JavaScript中的一些函数与对象(1)

    前端学习 第二弹: JavaScript中的一些函数与对象(1) 1.apply与call函数 每个函数都包含两个非继承而来的方法:apply()和call(). 他们的用途相同,都是在特定的作用域中 ...

  8. JavaScript学习09 函数本质及Function对象深入探索

    JavaScript学习09 函数本质及Function对象深入探索 在JavaScript中,函数function就是对象. JS中没有方法重载 在JavaScript中,没有方法(函数)重载的概念 ...

  9. JavaScript学习总结-技巧、有用函数、简洁方法、编程细节

    整理JavaScript方面的一些技巧.比較有用的函数,常见功能实现方法,仅作參考 变量转换 //edit http://www.lai18.com var myVar = "3.14159 ...

随机推荐

  1. 混淆矩阵-MATLAB代码详解

    一.混淆矩阵 (一).简介 在人工智能中,混淆矩阵(confusion matrix)是可视化工具,特别用于监督学习,在无监督学习一般叫做匹配矩阵.在图像精度评价中,主要用于比较分类结果和实际测得值, ...

  2. webpack 4.x 从零开始初始化一个vue项目

    创建目录 项目名称: vue-init app css reset.sass js home index.vue router index.js main.js App.vue views index ...

  3. [Mathematics][BJTU][Calculus]Detailed explanations and proofs of the Dirac-Abel Discriminant Methods which deal with the conditional convergence

    So, today we will talk about the conditional convergence and two discriminant methods, namely Dirac- ...

  4. std::unique_ptr的用法

    std::ofstream("demo.txt") << 'x'; // 准备要读的文件 { std::unique_ptr<std::FILE, decltyp ...

  5. Unity3D for iOS初级教程:Part 3/3(上)

    转自:http://www.cnblogs.com/alongu3d/archive/2013/06/01/3111738.html 欢迎来到第三部分,这是Unity 3D for iOS初级系列教程 ...

  6. LightOJ1355 Game Of CS(green 博弈)

    Jolly and Emily are two bees studying in Computer Science. Unlike other bees they are fond of playin ...

  7. BZOJ 3107 [cqoi2013]二进制a+b (DP)

    3107: [cqoi2013]二进制a+b Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 995  Solved: 444[Submit][Stat ...

  8. CodeForces845G-Shortest PathProblem?

    You are given an undirected graph with weighted edges. The length of some path between two vertices ...

  9. 【CSS】340- 常用九宫格布局的几大方法汇总

    对,就是类似这样的布局~ 目录 1  margin负值实现 2  祖父和亲爹的里应外合 3  换个思路 - li生个儿子帮大忙 4 借助absolute方位值,实现自适应的网格布局 5 cloumn多 ...

  10. 【GTK编程】安装与测试

    版权声明:本文为博主原创文章,转载请注明出处. https://www.cnblogs.com/YaoYing/ 前言 领导让做个类似平板触摸的GUI程序,通过触摸两块区域,实现背景图片的左右切换.本 ...