前端--关于javascript函数
终于可以说说函数了,函数是javascript设计最出色的地方,可以说它是所有概念中最重要的一个,因为围绕函数而阐述的周边概念涵盖了javascript的方方面面,所以理解了函数可以说对javascript有了大半部分的理解,在此我只能怀着谨慎与谦逊的态度小心翼翼的总结出自己所理解的函数。
- 函数是什么
要理解javascript函数首先得弄明白它是什么,这个问题并不是很容易就让人接受的,尤其是对有过C语言或者java等编程经验的人来说,因为会受到函数声明写法的诱惑,认知更倾向于把它理解为一种编程方式,一种可以重复调用执行的具名语句块。而在javascript中,函数是一种数据类型不是一种编程方式,它是对象类型的实例,它就是对象,它就是一种值,它和{x:3}这种对象直接量的地位是一样的。这种对象的定义有它自己特殊的写法,解释器在进行词法分析的时候会对此进行判断,它的可重复执行的语句块只是这个对象的一个隐藏属性,而要调用这个隐藏属性的执行只需要用到该对象加()运算符即可。
- 函数定义
函数定义有三中方法:定义表达式、函数声明语句、Function构造函数。
定义表达式从字面上就可以看出它是一个表达式,塔仅仅返回一个函数对象值,主要用在给变量赋值等需要用到值的地方,它的写法和函数声明语句是差不多的,都是function关键字+标识符+()+{},只是这个标识符可以省略,省略标识符的函数也叫做匿名函数,不省略的话就只是在函数体内可以访问的一个指向函数本身的局部变量,一般情况下是省略的。
而函数声明语句就相当于一条赋值语句,可以看作是把变量声明与函数对象值的赋值合并在一起的写法,所以function后面的标识符是不能省略的,并且这个标识符在函数体外面可以访问到。但函数声明语句毕竟是有别于一般的函数赋值语句的,最重要的一条区别就是变量声明提前。函数声明语句在程序执行到函数声明之前时是可以调用该函数的,例如fun1();function fun1(){};这样在声明fun1之前调用fun1是不会报错的。函数赋值语句则不行,函数赋值语句要在给变量赋值之后才能调用该变量,否则会报错,例如fun2(); var fun2=function (){};这样提前调用就会报错。除此之外声明语句和定义表达式还有一个不同就是他俩可以出现的地方:函数声明语句是不能够出现在for循环、if/else、try/catch等语句中的,而定义表达式则可以出现在程序的任何地方。
第三种Function构造函数定义函数的方法在实际编程中是不经常使用的,它允许javascript在运行时动态地创建并编译函数,并且它动态创建的函数中this是指向全局对象的。
- 函数调用
所谓函数调用就是执行函数对象中隐藏属性指向的代码块,这需要用到一个运算符--(),语法规则就是函数对象+()。
虽然函数调用的语法是唯一的即函数对象+(),但函数调用的模式或者调用场景却可以有些不同,在javascript中一共有四种调用模式:函数调用、方法调用、构造函数调用、间接调用、事件回调。这五种调用模式的差别就是函数体中初始化this的取值不同。(this不是变量也不是属性名,它是一个关键字,不能给它赋值)
函数调用很常见也很简单,就是函数对象+()调用,函数体中的this关键字取值初始化为全局对象,要注意的是这里也包括嵌套函数的调用,不要想当然的认为嵌套函数的函数体中this的值会初始化为父函数对象。
方法调用比函数调用多了一步对象属性的取值过程,因为只有当函数被保存为一个对象的属性时才叫做方法,函数体中this关键字取值初始化为函数属性所属的对象。如果一个对象有多个方法,并且每个方法的方法体最后又都返回this,这样就可以启用级联,一个方法可以调用另一个方法形成方法链。同理,方法体中的嵌套函数中this值也是全局对象。
构造函数调用就是在函数调用前加一个new关键字,构造函数总是会返回一个对象并且函数体中this关键字取值初始化为这个返回的对象。这里要注意的是如果是一个方法构造函数,例如 new o.fun(),那么函数体里的this值仍然会被初始化为新建的对象而不是调用对象o。
借用调用也叫做间接调用,这需要用到函数对象从Function.prototype继承的方法:call、apply、bind,它们可以根据需要指定方法体内this的值。其中apply和call是立即执行原始方法的,只是他俩传入的参数形式有所不同,例如f.call(o,1,2,3),f.apply(o,[1,2,3]),f中this的值为o传入的参数是1,2,3;注意apply传参数的时候不仅仅可以是真实的数组也可以是类数组,比如arguments或者getElementsByTagName。bind方法并不是立即执行的而是返回一个代理函数,例如var a = f.bind(o),这个代理函数a可以在需要的时候再被调用,在调用的时候所有传递给a的参数都将传入原始函数调用执行(这就是为什么它是一个代理函数,它甚至连prototype属性都没有)其中的this初始化为先前绑定的对象o。
事件回调就是在dom编程中,由浏览器触发某个事件时而调用对函数,此时函数体中的this值为触发dom对象的引用。也是一样嵌套函数中的this也是指向window对象
- 函数参数
在定义函数的时候可以在括号里写入形式参数,在函数调用时传入的实参会初始化相应的形参。因为在javascript中函数调用的时候是不会检查函数签名的,不论传入何种类型的实参函数体都会执行,而且不管多少个参数都总是会传入到函数体内。其中的一个原因就是每次调用时都会为函数体传入两个隐形参数:this和arguments,this的取值前面已经说过了,arguments是一个类数组对象,它的属性包括了调用者传入的所有参数。要访问传入的实参值只需要通过arguments加下标就可以,例如arguments[2],这里要注意的是如果定义了形参,那么形参和相应arguments的属性的关系是别名的关系,就好像引用同一个对象一样,如果改变了形参值,相应的arguments值也会改变,即便这个值是原始类型。要查看调用者传入了几个参数只需要调用arguments.length属性,这个属性表示了实参个数,arguments除了具有length属性,还有callee和caller属性,callee属性指向当前运行的函数对象,caller指向调用当前正在执行的函数的函数,只是caller还并不是官方标准。
- 函数属性
函数作为一个对象它也是有默认属性的:length和prototype。函数的length属性表示当初定义式写的形参个数,prototype是原型对象,用作构造函数时构造的对象继承这个prototype属性值,通过为prototype添加新方法可以拓展内置类型或者原始类型的功能。同时也可以为函数自定义属性,比如随便写一个function a(){a.coun++;},a.count=0,这里就比较有意思了,因为每调用一次a(),a.count就加1,在每次调用中共享的都是同一个a.count,为什么比较有意思呢,因为函数每次调用的时候执行的都是该函数的副本,感觉不应该共享a.count,既然事实上是共享的了,那就说明函数对象a只有一个是不会被复制的,副本指的是代码块的副本,并且在每一个代码块副本的执行过程中都可以访问到那同一个函数对象的属性,这是理解作用域和闭包的一个点。
- 函数作用域
在一段程序中某个地方用到的变量名并不总是如你所愿可以取到值或者取到你所预测的值,因为在程序中一个变量名字的可用范围和生命周期是有限的,这个有效的范围与生命周期就是作用域。根据这种特点可以把变量划分为全局变量和局部变量,全局变量就是变量名在从程序开始到结束这整个代码范围内都是有效的,局部变量只是变量名在整个程序代码的某一段范围内是有效的。其他的程序设计语言在一般情况下一个{}括起来的代码块就可以划出一个局部作用域范围,在{}中声明的任何变量都只在{}里有效,在{}外面生命周期就停止了,这有个学名叫块级作用域。然而在javascript中并不支持块级作用域,也就是说javascript不支持这种划分局部作用域的模式,javascript有自己特有的划分局部作用域的模式,可以说javascript只有这一种划分作用域的模式,就算全局变量也是这种划分模式的应用。应该知道的是在javascript运行任何代码之前也就是在javascript解释器启动运行的时候(在前端中就是页面加载的时候),它会创建一个叫做全局对象的对象,这个对象就相当于javascript的类库,里面有编程时常用的内置方法和属性,比如Array、Date、parseInt、Math等,同时自己写的所有源代码也是这个全局对象的一个隐藏属性,就好像函数对象的函数体语句块是函数的一个隐藏属性一样,所以可以把这个全局对象看作是一个函数对象,整个javascript源代码的执行就是调用这个全局函数对象,在这个全局函数对象的函数体中声明的变量在源代码中任何地方都是可见的,是全局变量。同样如果要创建一个局部变量,只需要创建一个函数对象,在这个函数对象的函数体中声明的变量在函数体执行时只在该函数体内部可见,函数执行结束后在函数体外面访问不到声明的变量,所以可以说函数体语句块是javascript划分作用域的唯一模式---即函数作用域模式。这种函数作用域比前面的块级作用域要复杂,块级作用域的实现是依靠对内存栈的操作,而函数作用域则是函数体内通过遍历查找函数对象的某个隐藏属性来实现的。这里要解释的是在函数对象被定义的时候javascript引擎还为其添加了一些仅供解释器存取的内部属性,姑且就叫它[scope]属性吧,这个隐藏属性的值是一个对象,这个对象类似于一个数组,下标以0开始,每个“数组“元素都是一个对象,比方说表达式scope[2]返回一个对象,scope[n]返回的对象按键值对的形式存储函数体可以用到的部分变量的名字和值,因此可以说这个scope对象是一个按顺序排列的一系列对象的集合,集合中的每个子对象存储了可用的变量名和变量值,所以这个集合就是常说的作用域链。当在函数体内遇到变量名字的时候,解释器就会按顺序遍历这个集合也就是scope对象(在函数属性中提到过函数体副本可以访问函数对象的属性)来查找同名的对象属性,找到了就取相应的值找不到就报错,这个过程叫标识符的解析。
在标识符解析过程之前还存在两个步骤:scope对象的初始化和“执行环境”对象的初始化。scope对象的初始化过程有一个好听的名字叫词法作用域,词法作用域讲的是通过阅读源代码看定义变量时的环境就能知道一个变量的作用域或者看一个函数的定义环境就可以知道它有哪些变量可用,换句话就是说在定义函数的时候scope对象就会被初始化,例如定义一个全局函数function globalFun(){}(这个函数定义在全局环境中),当解释器扫过这句话的时候会把“全局对象”(这个全局对象代表着所有全局范围内定义的变量)赋值给globalFun对象的scope[0]。当这个函数被调用执行函数体之前有一个“执行环境”对象初始化的过程,之前说过函数在每次调用的时候执行的都是函数体的副本,解释器会为每一个副本创建一个唯一的“执行环境”对象,所以如果多次调用会创建多个“执行环境”对象。每个“执行环境”对象的初始化分为两步:1.复制函数对象的scope属性值到“执行环境”对象;2.创建一个名字叫做“活动对象”的对象,把这个“活动对象”的属性名初始化为形参、局部变量名,而属性值是在不同阶段分开赋值的(暂不详述);并且这个“活动对象”放到这个“执行环境”对象的最前面,函数执行过程中函数体内标识符解析过程用到的作用域链都在这个“执行环境”对象里面。
- 闭包
不少和我一样的初学者觉得会觉得闭包很难不好理解,我认为主要的原因是这个名字起的太难听了--!
其实只要明白了闭包产生的原因闭包就很好理解了,产生闭包的原因有两个:词法作用域和垃圾回收机制。
前面讲了在全局环境下函数作用域链的创建,而当在一个函数体环境下声明一个嵌套函数时情况会有所不同,嵌套函数的scope属性的会被初始化为引用父函数的“执行环境”对象,所以当嵌套函数被调用的时候它的执行环境对象里包含了父函数的局部变量。javascript中的垃圾回收机制是一种自动的回收,当内存中的对象不存在被引用或者变量不在被使用的时候他们会被js引擎销毁以释放内存,所以嵌套的函数被父函数当作结果返回的时候,如果嵌套函数用到了父函数的局部变量,那么在父函数结束调用的时候它的执行环境对象不会被销毁,它会在嵌套函数的scope属性值中继续存在,所以在调用嵌套函数的时候仍然可以在嵌套函数的“执行环境”对象中访问到父函数的局部变量。到这我仍的然不知道什么是闭包,有的地方说这个嵌套函数就是闭包,也有的地方说嵌套函数的方法块是闭包,还有的地方说这种变量可以保存到作用域中的特性是闭包,不管咋样反正闭包就是这么个事情。有一点要注意的是闭包可能会产生性能问题,因为父函数的“执行环境”对象没有释放会占据更多的内存空间,并且在进行标识符解析的过程中父函数的变量在嵌套函数中“执行环境”的第二层对象,所以在遍历的时候会总是多遍历当前嵌套函数的活动对象一次而产生性能的损耗。
- 柯里化
柯里化是一种对闭包的应用,它的作用是把多参数函数转换为一系列单参数函数并进行调用。一个最简单极端的例子,实现一个求两个数的和的函数,没有柯里化的函数定义是function add(x+y){return x+y;},这个函数在调用的时候要一次性传入两个参数,add(1,2);而柯里化的写法是function add(x){return function(y){return x+y}},这个函数在调用的时候要分次传入一个参数,add(1)(2);这种写法通用性很差,一个通用性比较好的柯里化方法应该是一个无参函数,并且要每次调用都要保存住传入的参数,所以可以写一个拓展方法Function.prototype.curry(){var slice=Array.prototype.slice;var args=slice.call(arguments;var that=this;return function(){return that.apply(null,args.concat(slice.apply(arguments)));});}。好在EMCAScript5标准之后不用这么麻烦,5标准有一个bind方法,前面说过这个方法属于函数的借用调用,bind不直接执行而是返回一个初始化了this的函数,其实bind不仅可以初始化this,还可以初始化其他的参数,所以bind方法第一个参数用来初始化this,可以为null,其后的参数都是初始化函数参数的,因此那个add函数可以这么调用add.bind(null,1)(3);
前端--关于javascript函数的更多相关文章
- 前端控制台 JavaScript函数报错 SyntaxError: expected expression, got ';' SyntaxError: expected expression, got 'if'
在火狐浏览器下调试时, 页面报错SyntaxError: expected expression, got ';'或者SyntaxError: expected expression, got 'if ...
- 前端笔试题目总结——应用JavaScript函数递归打印数组到HTML页面上
数组如下: var item=[{ name:'Tom', age:70, child:[{ name:'Jerry', age:50, child:[{ name:'William', age:20 ...
- web前端学习(四)JavaScript学习笔记部分(3)-- JavaScript函数+异常处理+事件处理
1.Javascript函数-了解函数的用途 1.1.函数: 函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块 2.Javascript函数-定义函数 2.1.function必须小写 3. ...
- 前端基础之javaScript(函数)
目录 一:javaScript函数 1.函数关键字 2.格式 3.无参函数 4.有参函数 二:关键字arguments 1.能够获取函数接受的索引参数 2.关键字arguments(限制函数不能多也不 ...
- 前端之JavaScript基础
前端之JavaScript基础 本节内容 JS概述 JS基础语法 JS循环控制 ECMA对象 BOM对象 DOM对象 1. JS概述 1.1. javascript历史 1992年Nombas开发出C ...
- 互联网公司前端初级Javascript面试题
互联网公司前端初级Javascript面试题 1.JavaScript是一门什么样的语言,它有哪些特点?(简述javascript语言的特点)JavaScript是一种基于对象(Object)和事件驱 ...
- 第三篇:web之前端之JavaScript基础
前端之JavaScript基础 前端之JavaScript基础 本节内容 JS概述 JS基础语法 JS循环控制 ECMA对象 BOM对象 DOM对象 1. JS概述 1.1. javascript ...
- Javascript 函数及其执行环境和作用域
函数在javascript中可以说是一等公民,也是最有意思的事情,javascript函数其实也是一个对象,是Function类型的实例.因此声明一个函数首先可以使用 Function构造函数: va ...
- javascript函数定义以及常见用法
我们知道,js函数有多种写法,函数声明 ,函数表达式,Function式构造函数,自执行函数,包括Es6的箭头函数,Class类写法,高阶函数,函数节流/函数防抖,下面我就 ...
随机推荐
- SqlServer表属性查询
获得表信息: select syscolumns.name as field, syscolumns.isnullable as nullis, systypes.name as sqltype, s ...
- C# viewstate
Viewstate 处理不是form中可以传值的标签 进行传值.可以禁用元素的viewstate 也可以禁用页面的Enableviewstate=”false”;(在配置区域写)内网系统,互联网后台可 ...
- AngularJs练习Demo2
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport&quo ...
- WCF入门教程系列六
一.前言 前面的几个章节介绍了很多理论基础,如:什么是WCF.WCF中的A.B.C.WCF的传输模式.本文从零开始和大家一起写一个小的WCF应用程序Demo. 大多框架的学习都是从增.删.改.查开始来 ...
- Java中构造方法的执行顺序
一.先执行内部静态对象的构造方法,如果有多个按定义的先后顺序执行:静态对象在构造的时候也是也先执行其内部的静态对象. 二.再调用父类的构造方法(父类还有父类的话,从最开始的基类开始调用),如果没有明显 ...
- 编译XSIP过程中环境配置
昨天在编译XSip的过程中,有很多问题首先是出现了很多的error C1083. 然后到XSIP自己的文件夹中,也找不到对应的.h文件. 上网查阅后发现应该是缺少了对应的头文件的路径. 于是到可以 ...
- GetClientRect()和GetWindowRect()
GetClientRect() 是得到客户区坐标系下客户区的RECT GetWindowRect()是得到屏幕坐标系下整个窗口的RECT GetSystemMetrics()是获得屏幕分辨率大小 ...
- PHP中的魔术方法总结 :__construct, __destruct , __call, __callStatic,__get, __set, __isset, __unset , __sleep, __wakeup, __toStr
PHP中的魔术方法总结 :__construct, __destruct , __call, __callStatic,__get, __set, __isset, __unset , __sleep ...
- Android再学习-20141018-布局-进度条
20141018-Android再学习 对齐至控件的基准线 为了保证印刷字母的整齐而划定的线(四线三格的第三条线). android:layout_alignBaseline 与父控件的四个边缘对齐( ...
- LeetCode_Simplify Path
Given an absolute path for a file (Unix-style), simplify it. For example, path = "/home/", ...