javascript函数全解
栏目: JavaScript · 发布时间: 6年前
内容简介:本文总结了js中函数相关的大部分用法,对函数用法不是特别清晰的同学可以了解一下。同其他语言不同的是,js中的函数有2种含义。普通函数:同其他语言的函数一样,是用于封装语句块,执行多行语句的语法结构。
本文总结了js中函数相关的大部分用法,对函数用法不是特别清晰的同学可以了解一下。
1.0 简介
同其他语言不同的是,js中的函数有2种含义。
普通函数:同其他语言的函数一样,是用于封装语句块,执行多行语句的语法结构。
构造函数:不要把它当作函数,把它当作class,内部可以使用this表示当前对象。
【注】后续代码基于ES6&ES7标准,笔者是在nodejs v10.7.0环境下运行(你也可以选择其他支持ES6的node版本)。
1.1 函数的声明
虽然普通函数和构造函数,含义有所不同,可是声明方法却完全一样。
1.1.0 函数声明
function sort(arr) { let ret = [...arr]; let length = ret.length; for (let i = 0; i < length; i++) { for (let j = i + 1; j < length; j++) { if (ret[i] > ret[j]) { [ret[j], ret[i]] = [ret[i], ret[j]]; } } } return ret; } 复制代码
1.1.1 函数表达式
let sort = function (arr) { let ret = [...arr]; ... ... return ret; } 复制代码
函数表达式和普通函数声明的区别在于,普通函数声明会 提升
,函数表达式不会 提升
。
“提升”的意思是说: 在函数声明前就可以调用这个函数。不必先声明后调用。
js会在运行时,将文件内所有的 函数声明
,都提升到文件最顶部,这样你可以在代码任意位置访问这个函数。
而现在根据ES6标准,使用 var
修饰的函数表达式会 提升
,使用 let
修饰的则不会提升。
1.1.2 使用Function构造函数声明
let sort = new Function("arr", ` function sort(arr) { let ret = [...arr]; let length = ret.length; for (let i = 0; i < length; i++) { for (let j = i + 1; j < length; j++) { if (ret[i] > ret[j]) { [ret[j], ret[i]] = [ret[i], ret[j]]; } } } return ret; } `); 复制代码
这种使用Function构造方法创建的函数,同 函数声明
产生的函数是完全相同的。
构造函数接收多个字符串作为参数,最后一个参数表示 函数体
,其他参数表示 参数名
。
像上面这个例子和1.1.0中的声明完全相同。
这种声明方式,没有发现有什么优点,并不推荐使用。
1.2 闭包
闭包,简单说就是在函数中声明的函数,也就是嵌套函数。它能够延长父作用域部分变量的生命周期。
闭包可以直接使用其所在函数的任何变量,这种使用是 引用传递
,而不是 值传递
,这一点很重要。
let f = function generator() { let arr = [1, 2, 3, 4, 5, 6, 7]; let idx = 0; return { next() { if (idx >= arr.length) { return { done: true }; } else { return { done: false, value: arr[idx++] }; } } } } let gen = f(); for (let i = 0; i < 10; i++) { console.log(gen.next()); } 复制代码
上面的代码中, generator
函数中的闭包 next()
可直接访问并修改所在函数中的变量 arr
和 idx
。
一般说来,闭包需要实现尾递归优化。
尾递归是指,如果一个函数,它的最后一行代码是一个闭包的时候,会在函数返回时,释放父函数的栈空间。
这样一来,依赖闭包的递归函数就不怕栈溢出了(nodejs在64位机器上可达到1万多层的递归才会溢出,有可能是根据内存情况动态计算的)。
ES6明确要求支持尾递归。
而据网络上资料说,nodejs需要在严格模式下,使用--harmony选项,可以开启尾递归。
然而我使用下列代码发现,并没有开启(nodejs版本为v10.3.0)。
// File: test.js // Run: node --harmony test.js "use strict" function add(n, sum) { if (n == 0) { console.trace(); return sum; } else { return add(n - 1, sum + n); } } console.log(add(10, 0)); /* 输出为: Trace at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:5:11) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10) 55 */ 复制代码
1.3 匿名函数
我们经常在js的代码中看见下面这种写法:
(function(){ ... ... ... })(); 复制代码
将一个匿名函数直接执行,如果刚接触js的同学可能觉得这是脱裤子放屁。
但是这个匿名函数的最大作用在于作用域隔离,不污染全局作用域。
如果没有匿名函数包裹,代码中声明的所有变量都会出现在全局作用域中,造成不必要的变量覆盖麻烦和性能上的损失。
ES6中这种写法可以抛弃了,因为ES6引入了 块作用域
:
{ ... ... ... } 复制代码
作用和上面的匿名函数相同。
另外ES6中增加了一种匿名函数的写法:
//ES6以前的写法 function Teacher(name){ this.name = name; var self = this; setTimeout(function(){ console.log('Teacher.name = ' + self.name); }, 3000); } //现在这样写 function Student(name){ this.name = name; setTimeout(() => { console.log('Student.name = ' + this.name); }, 3000); } 复制代码
新的匿名函数的在写法上有2处不同:
function =>
而它也带来了一个巨大的好处:
匿名函数中的 this
对象总是指向声明时所在的作用域的 this
,不再指向调用时候的 this
对象了。
这样我们就可以像上面的例子那样,很直观地使用this,不用担心出现任何问题。
所以比较强烈推荐使用新的匿名函数写法。
1.4 构造函数和this
1.4.1 基本面向对象语法
下面来介绍构造函数,js没有传统面向对象的语法,但是它可以使用函数来模拟。
了解js面向对象机制之前,可以先看一下,其他标准面向对象语言的写法,比如java,我们声明一个类。
class Person{ //构造函数 Person(String name, int age){ this.name = name; this.age = age; Person.count++; } //属性 String name; int age; //setter&getter方法 String getName(){ return this.name; } void setName(String name){ this.name = name; } int getAge(){ return this.age; } void setAge(int age){ this.age = age; } //静态变量 static int count = 0; //静态方法 public int getInstanceCount(){ return Person.count; } } 复制代码
由此可知,一个类主要包含如下元素: 构造函数
, 属性
, 方法
, 静态属性
, 静态方法
。
在js中,我们可以使用js的 构造函数
,来完成js中的面向对象。
js的 构造函数
就是用来做面向对象声明(声明 类
)的。
构造函数
的声明语法同普通函数完全相同。
//构造函数 function Person(name, age){ //属性 this.name = name; this.age = age; //setter&getter this.getName = function(){ return this.name; } this.setName = function(name){ this.name = name; } this.getAge = function(){ return this.age; } this.setAge = function(age){ this.age = age; } Person.count++; } //静态变量 Person.count = 0; //静态方法 Person.getInstanceCount = function(){ return Person.count; } 复制代码
可以发现, 构造函数
中同 普通函数
相比,特别的地方在于使用了 this
,同其他面向对象的语言一样, this
表示当前的实例对象。
把我们用 js
声明的类与 java
的类相对比,二者除了写法不同之外,上述关键元素也都包含了。
1.4.2 prototype
js使用上面的方法声明了类之后,就可以使用 new
关键字来创建对象了。
let person = new Person("kaso", 20); console.log("person.name=" + person.getName() + ", person.age=" + person.getAge()); //输出:person.name=kaso, person.age=20 let person1 = new Person("jason", 25); console.log("person.name=" + person.getName() + ", person.age=" + person.getAge()); //输出:person.name=jason, person.age=25 复制代码
创建对象,访问属性,访问方法,都没问题,看起来挺好的。
但是当我们执行一下这段代码,会发现有些不对:
console.log(person.getName === person1.getName); //输出:false 复制代码
原来构造函数在执行的时候,会将所有成员方法,为每个对象生成一份copy,而对于类成员函数来说,保留一份copy就足够了,而不同的对象可以用this来区分。上面的做法很明显,内存被白白消耗了。
基于上述问题,js引入了prototype关键字并规定:
存储在prototype中的方法和变量可以在类的所有对象中共享。
因此,上面的构造函数可以修改成这样:
function Person(name, age){ this.name = name; this.age = age; Person.count++; } Person.prototype.getName = function(){ return this.name; } Person.prototype.setName = function(name){ this.name = name; } Person.prototype.getAge = function(){ return this.age; } Person.prototype.setAge = function(age){ this.age = age; } Person.count = 0; Person.getInstanceCount = function(){ return Person.count; } 复制代码
运行效果和之前的写法相同,只是这次创建不同的对象时,成员方法不再创建多个副本了。
需要注意的是,成员变量不需要放到prototype中,可以想想为什么。
1.4.3 apply和call
js函数中绕不过的一个问题就是,方法里面的this到底指向哪里?
最官方的说法是:this指向调用此方法的对象。
对于类似于 java 这种面向对象的语言来讲,this永远指向所在类的对象实例。
对于js中也是这样,如果我们规规矩矩地像上一节介绍的那样使用,this也会指向所在类的对象实例。
但是,js也提供了更为灵活的语法,它可以让一个方法被不同的对象调用,即使不是同一个类的对象,也就是可以将同一个函数的this,设为不同的值。
这是一个极为灵活的语法,可以完成其他语言类似 接口(interface)
, 扩展(extension)
, 模版(template)
的功能。
实现此功能的方法有2个: apply
和 call
,二者实现的功能完全相同,即改变函数的this指向,只是函数传递参数方式不同。
call
接受可变参数,同函数调用一样,需将参数一一列出。
apply
只接受2个参数,第一个就是新的this指向的对象,第二个参数是原参数用数组保存起来。
代码如下:
let obj = { print(a, b, c){ console.log(`this is obj.print(${a}, ${b}, ${c})`); } } let obj1 = { print(a, b, c){ console.log(`this is obj1.print(${a}, ${b}, ${c})`); } } function test(a, b, c){ this.print(a, b, c); } test.apply(obj, [1, 2, 3]); test.call(obj, 4, 5, 7); test.apply(obj1, [1, 2, 3]); test.call(obj1, 4, 5, 7); /* 输出: this is obj.print(1, 2, 3) this is obj.print(4, 5, 7) this is obj1.print(1, 2, 3) this is obj1.print(4, 5, 7) */ 复制代码
1.4.4 继承
面向对象3大特征:封装,继承,多态,其中最重要的就是继承,多态也依赖于继承的实现。可以说实现了继承,就实现了面向对象。
java中的继承很简单:
class Student extends Person{ ... ... } 复制代码
Student继承之后自动获得Person的所有成员变量和成员方法。
因此,我们在实现js继承的时候,主要就是获取到父类的成员变量和成员方法。
最简单的实现就是,将父类的成员变量和方法直接copy到子类中。
这需要做2件事:
- 为了copy成员方法,可以将Student的prototype指向父类的prototype
- 为了copy成员属性,子类构造函数需要调用父类构造函数
function Student(name, age){ Person.call(self, name, age); } Student.prototype = Person.prototype; 复制代码
上面代码可以达到继承的目的,但是会产生两个问题
- 如果我向Student中添加新的成员方法时,会同时加入到父类中
- 多层次继承无法实现,即当所调用的方法在父类中找不到的时候,不会去父类的父类中去查找
所以我们不能直接将Person.prototype直接给Student.prototype。
经过思考,一个可行方案是,令子类prototype指向父类的一个对象,即像这样:
Student.prototype = new Person(); 复制代码
这样做,可以解决上面的2个问题。
但是它仍然有些瑕疵:会调用2次父类构造函数,造成一定的性能损失。
所以我们的终极继承方案是这样的:
function Student(name, age){ Person.call(self, name, age); } function HelpClass(){} HelpClass.prototype = Person.prototype; Student.prototype = new HelpClass(); 复制代码
上面关键代码的意义在于,用一个空的构造函数代替父类构造函数,这样调用了一个空构造函数的代价会小于调用父类构造函数。
另外上述代码可以用Object.create函数简化:
function Student(name, age){ Person.call(self, name, age); } Student.prototype = Object.create(Person.prototype); 复制代码
这就是我们最终的继承方案了。可以写成下面的通用模式。
function extend(superClass){ function subClass(){ superClass.apply(self, arguments); } subClass.prototype = Object.create(superClass.prototype); return subClass; } let Student = extend(Person); let s = new Student('jackson', '34'); console.log("s.getName() = " + s.getName() + ", s.getAge() = " + s.getAge()); //输出为:s.getName() = jackson, s.getAge() = 34 复制代码
当然实现一个完整的继承还需要完善其他诸多功能,在这里我们已经解决了最根本的问题。
1.5 generator函数和co
generator是ES6中提供的一种异步编程的方案。有点像其他语言(lua, c#)中的协程。
它可以让程序在不同函数中跳转,并传递数据。
1.5.1 基本用法介绍
看下面的代码:
function *generatorFunc(){ console.log("before yield 1"); yield 1; console.log("before yield 2"); yield 2; console.log("before yield 3"); let nextTransferValue = yield 3; console.log("nextTransferValue = " + nextTransferValue); } let g = generatorFunc(); console.log("before next()"); console.log(g.next()); console.log(g.next()); console.log(g.next()); console.log(g.next(1024)); /*输出: before next() before yield 1 { value: 1, done: false } before yield 2 { value: 2, done: false } before yield 3 { value: 3, done: false } nextTransferValue = 1024 { value: undefined, done: true } */ 复制代码
可以看到generator函数有3要素:
* yield next()
另外还有一些其他规则:
-
generator函数内的第一行代码,需要在第一个
next()
执行后执行 -
函数在执行
next()
时,停顿在yield
处,并返回yield
后面的值,yield
后的代码不再执行。 -
next()
返回的形式是一个对象:{value: XXX, done: false}
,这个对象中,value
表示yield
后面的值,done
表示是否generator函数已经执行完毕,即所有的yield
都执行过了。 -
next()
可以带参数,表示将此参数传递给上一个yield
,因为上次执行next()
的时候,代码停留在上次yield
的位置了,再执行next()
的时候,会从上次yield
的位置继续执行代码,同时可以令yield
表达式有返回值。
从上述介绍中可以看出,generator除了在函数中跳转之外,还可以通过 next()
来返回不同的值。
了解过ES6的同学应该知道,这种 next()
序列,特别符合
迭代器
的定义。
因此,我们可以很容易把generator的函数的返回值组装成数组,还可以用 for..of
表达式来遍历。
function *generatorFunc(){ yield 1; yield 2; yield 3; } let g = generatorFunc(); for(let i of g){ console.log(i); } /* 输出: 1 2 3 */ 复制代码
function *generatorFunc(){ yield 1; yield 2; yield 3; } let g = generatorFunc(); console.log(Array.from(g)); /* 输出: [1, 2, 3] */ 复制代码
除了上述规则外,generator还有一个语法 yield *
,它可以连接另一个generator函数,类似于普通函数间调用。用于一个generator函数调用另一个generator函数,也可用于递归。
function *generatorFunc(){ yield 3; yield 4; yield 5; } function *generatorFunc1(){ yield 1; yield 2; yield * generatorFunc(); yield 6; } let g = generatorFunc1(); console.log(Array.from(g)); /* 输出: [1, 2, 3, 4, 5, 6] */ 复制代码
除了获取数组外,我们还可以使用generator的 yield
和 next
特性,来做异步操作。
js中的异步操作我们一般使用Promise来实现。
请看下列代码及注释。
let g = null; function *generatorFunc(){ //第一个请求,模拟3s后台操作 let request1Data = yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("123"); }, 3000); }).then((d) => { //令函数继续运行,并把promise返回的数据通过next传给上一个yield,代码会运行到下一个yield g.next(d); }); //输出第一个请求的结果 console.log('request1Data = ' + request1Data); //同上,开始第二个请求 let request2Data = yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("456"); }, 3000); }).then((d) => { g.next(d); }); //第二个请求 console.log('request2Data = ' + request2Data); } g = generatorFunc(); g.next(); console.log('completed'); /* 输出: completed(马上输出) request1Data = 123(3s后输出) request2Data = 456(6s后输出) */ 复制代码
我们换一种写法:
let g = null; function *request1(){ return yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("123"); }, 3000); }).then((d) => { g.next(d); }); } function *request2(){ return yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("456"); }, 3000); }).then((d) => { g.next(d); }); } function *generatorFunc(){ let request1Data = yield *request1(); console.log('request1Data = ' + request1Data); let request2Data = yield *request2(); console.log('request2Data = ' + request2Data); } g = generatorFunc(); g.next(); console.log('completed'); /* 输出同上 */ 复制代码
运行结果是相同的,所以我们可以看到,generator函数能够把异步操作写成同步形式,从而避免了回调地狱的问题。
异步变成同步,不知道能够避免多少因为回调,作用域产生的问题,代码逻辑也能急剧简化。
1.5.2 generator函数的自动运行
虽然我们可以通过generator消除异步代码,但是使用起来还是不太方便的。
需要把generator对象提前声明保存,然后还要在异步的结果处写 next()
。
经过观察发现,这些方法的出现都是有规律的,所以可以通过代码封装来将这些操作封装起来,从而让generator函数的运行,就像普通函数一样。
提供这样功能的是
co.js
(可以点这里跳转)
,大神写的插件,用于generator函数的自动运行,简单的说它会帮你自动执行 next()
函数,所以借助 co.js
,你只需要编写yield和异步函数即可。
使用 co.js
,上面的异步代码可以写成这样:
let co = require('./co'); function *request1(){ return yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("123"); }, 3000); }); } function *request2(){ return yield new Promise((resolve, reject) => { setTimeout(()=>{ resolve("456"); }, 3000); }); } function *generatorFunc(){ let request1Data = yield *request1(); console.log('request1Data = ' + request1Data); let request2Data = yield *request2(); console.log('request2Data = ' + request2Data); } co(generatorFunc); console.log('completed'); /* 输出同上 */ 复制代码
可以看到,借助 co.js
你只需要写yield就能够把异步操作写成同步调用的形式。
注意,请使用promise来进行异步操作。
1.6 async和await
使用 generator
+ Promise
+ co.js
可以较为方便地实现异步转同步。
而js的新标准中,上面的操作已经提供了语法层面的支持,并将异步转同步的写法,简化成了2个关键字: await
和 async
。
同样实现上节中的异步调用功能,代码如下:
async function request1(){ return await new Promise((resolve, reject) => { setTimeout(()=>{ resolve("123"); }, 3000); }); } async function request2(){ return await new Promise((resolve, reject) => { setTimeout(()=>{ resolve("456"); }, 3000); }); } async function generatorFunc(){ let request1Data = await request1(); console.log('request1Data = ' + request1Data); let request2Data = await request2(); console.log('request2Data = ' + request2Data); } generatorFunc(); console.log('completed'); /* 输出同上 */ 复制代码
await/async使用规则如下:
try-catch
await/async本身就是用来做异步操作转同步写法的,它的规则和用法也很明确,只要牢记上面几点,你就能用好它们。
//抛出异常的async方法 async function generatorFunc1(){ console.log("begin generatorFunc1"); throw 1001; } //async方法返回的是Promise对象,使用Promise.catch捕获异常 generatorFunc1().catch((e) => { console.log(`catch error '${e}' in Promise.catch`); }) //正常带返回值的async方法 async function generatorFunc2(){ console.log("begin generatorFunc2"); return 1002; } //async方法返回的是Promise对象,使用Promise.then获取返回的数据 generatorFunc2().then((data)=>{ console.log(`data = ${data}`); }) //await后带的async方法若抛出异常,可以在await语句增加try-catch捕获异常 async function generatorFunc3(){ console.log("begin generatorFunc3"); try{ await generatorFunc1(); }catch(e){ console.log(`catch error '${e}' in generatorFunc3`); } } generatorFunc3(); console.log('completed'); /* 输出: begin generatorFunc1 begin generatorFunc2 begin generatorFunc3 begin generatorFunc1 completed catch error '1001' in Promise.catch data = 1002 catch error '1001' in generatorFunc3 */ 复制代码
--完--
以上所述就是小编给大家介绍的《javascript函数全解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Python 拓展之特殊函数(lambda 函数,map 函数,filter 函数,reduce 函数)
- Python 函数调用&定义函数&函数参数
- python基础教程:函数,函数,函数,重要的事说三遍
- C++函数中那些不可以被声明为虚函数的函数
- 017.Python函数匿名函数
- 纯函数:函数式编程入门
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
One Click
Richard L. Brandt / Portfolio Hardcover / 2011-10-27 / 25.95
An insightful look at how Amazon really works and how its founder and CEO makes it happen. Amazon's business model is deceptively simple: make online shopping so easy and convenient that customers ......一起来看看 《One Click》 这本书的介绍吧!