函数
4.1 函数基本概念
为什么要讲函数的概念呢?都说基础很重要,只有到功夫深的时候才能体会到铁杵会磨成针的,所以说基础很重要。
何况函数在 javascript 中可是一等公民啊,就像立了大功的的大将一样,必须要给他颁发一个奖项,这个奖项就是 “一等公民”。可想而知他的重要性。
函数是既可以是事件驱动性质的,例如我们的按钮点击,需要给其绑定一个触发的事件,还有鼠标移入移除事件等等,也可以是被调用者调用的可重复使用的代码块。这里体现了函数的可重复使用的性质。
javascript 函数语法,函数就是包裹在花括号中的代码块,前面使用了关键词 function: 当调用该函数时,会执行函数内的代码。可以在某事件发生时直接调用函数(比如当用户点击按钮时),并且可由 JavaScript 在任何位置进行调用。
4.2 函数结构
function 函数名(arg1, arg2, arg3, ... , argn){
// 函数体
// 各种语句
// return 返回值
}
定义一个函数一个函数,首先我们必须以 function 这个关键字开头,然后是函数名,函数名必须以英文字母开头,接着就是一个小括号(),里面包含着各种各样的参数,然后就是一对花括号,里面包含着函数体,各种语句,最后一个就是函数返回值。
4.3 函数的声明方式
第一种:普通函数的声明
function fn(a, b) {
return a + b
}
function fn() {
console.log("fn")
}
这种普通的不能再普通了,一看就跟函数的结构一样,没什么好讲的。
第二种:函数表达式声明
这种跟函数声明的区别有以下三点:
- 以函数声明的方法定义的函数,函数名是必须的,而函数表达式的函数名是可选的
- 以函数声明的方法定义的函数,函数可以在函数声明之前调用,而函数表达式的函数只能在声明之后调用
- 以函数声明的方法定义的函数并不是真正的声明,他们仅仅可以出现在全局中或者嵌套在其它函数中
让我们来见证各种各样的函数表达式吧。
var fn = function(a,b) { // 普通变量赋值,将一个函数赋值给一个变量
return a + b
}
var fn = function() {
console.log("fn")
}
(function fn3(){ // 用()包裹一个普通函数变成一个表达式
})
(function(){ // 用()包裹 一个匿名函数
})
!function() { // 利用 逻辑非 运算符
}
~function func4(){ // 利用求反 运算符
}
true,function func6(){ // 使用bool 值 true
}
false,function func7() {
}
false||function fn() {
}
第四种:函数构造器
在JavaScript函数中第一个类(class object)对象: 函数是一个普通的对象类型是function。
构造器声明结构如下:
var fn = new Function(参数1,参数2,参数3,... ,参数n,函数体)
首先是一个关键字 new 接着是一个 Function 后面接着一堆参数,最后一个参数是指函数体,包括返回值等。
实例代码:
var number1 = 10
var number2 = 20
var fn = new Function(number1, number2, "return number1 + number2")
4.4 函数的调用方式
1. 作为一个函数调用
在一般模式下,其中 this 对象指向 windows
在严格模式下,其中 this 指向 undefined
Js 中 this 关键字很常见,但是它似乎变换莫测,让人抓狂。接下来就来揭示其中奥妙。借助阮一峰老师的话,他代表函数运行时,自动生成一个内部对象,只能在函数内部使用,这句话似乎很平常,可是要非常注意三个字,“运行时”这说明 this 关键字只与函数的执行环境有关,而与函数声明环境无关,也就会这个 this 到底代表的是什么是对象要等到函数执行时才知道,有点类似函数定义时的参数列表只在函数调用时才传入真正的对象,理解了这一点对后面 this 关键字的掌握有很大的帮助
function fn(a, b){
console.log(a+b)
console.log(this)
}
fn(1,2)
function fn(a, b) {
"use strict"; // 严格模式下
console.log(this)
}
fn(3,4)
- 函数作为方法调用
其中 this 指向方法所拥有的对象
var obj = {
name: "ken",
fn: function() {
console.log(this)
}
}
obj.fn()
- 使用构造函数调用
function fn(arg1,arg2){
this.firstName=arg1;
this.lastName=arg2;
console.log(this)
}
var x = new fn("Ken","Li");
x.firstName;
- 使用 call,apply调用
var obj;
function fn(a,b){
console.log(this)
return a*b;
}
obj = fn.call(obj , 3 , 4); //返回 12
function fn(a,b){
return a*b;
}
var arr=[3,4];
var obj = fn.apply(obj , arr) //返回12
- 自执行函数
(function() {
console.log(this)
})()
(function() {
"use strict";
console.log(this)
})()
4.5 函数返回值
一个函数的函数名即是该函数的代表,也是一个变量。由于函数名变量通常用来把函数的处理结果数据返回给调用函数,即递归调用,所以一般把函数名变量称为返回值,函数的返回值类型是在定义函数时指定的
1 每一个函数都有一个返回值
返回值结构:
//声明一个带返回值的函数
function 函数名(形参1, 形参2, 形参3...) {
//函数体
return 返回值;
}
//可以通过变量来接收这个返回值
var 变量 = 函数名(实参1, 实参2, 实参3...)
2 如果函数没有手动给其返回值,默认是返回 undefined
function fn() {
// 默认返回 undefined
}
3 如果显示给其返回值,则返回我们给的返回值
function fn(arg1, arg2) {
return arg1 + arg2
}
4 一旦函数返回,不管后面有多少条语句,都不会去执行了
function fn(arg1, arg2) {
return arg1 + arg2;
console.log(arg1+arg2)
}
5 函数返回值只能是单个值
// 错误实例
function fn(arg1, arg2){
return arg1, arg2 // 只返回 arg2
}
function fn(arg1, arg2) {
return (arg1, arg2) // 也是返回最后一个参数 arg2
}
6 如果实在要返回多个值,必须将多个参数转成对象,或者是数组
function fn(arg1, arg2) {
return [arg1, arg2]
}
function fn(arg1, arg2) {
return {
arg1: arg1,
arg2: arg2
}
}
4.6 函数中的 arguments
1 在非严格模式下,函数的参数都会储存在一个比较神奇的对象 arguments 中
2 在非严格模式下,在函数开始执行时,arguments的长度和实参传递的个数是一致的,假如实参少于形参,多出的形参并不会和arguments绑定,这些修改相互之间不影响。
function fn(arg1, arg2){
console.log(arguments.callee)
console.log(arguments.length)
console.log(arguments)
}
function fn(x, y, z) {
x = 100;
arguments[2] = 10; // 由于只传递两个实参,arguments[2]和z并无关联
z = 1; // 没有传递实参,和arguments无关联
console.log(arguments[0], arguments[1], arguments[2]);
console.log(arguments.length); // arguments 的长度取决于实参传递的个,后期修改无效
console.log(x, y, z);
}
f(3, 2);
// 我们可以对 arguments 进行遍历
function fn(arg1, arg2) {
for(var key in arguments){
console.log(arguments[key])
}
}
fn(1,2,3,4) // 1,2,3,4
3 在严格模式下,只有函数执行开始的相互映射,之后arguments和形参之间的修改互不影响。
function fn(arg1, arg2){
"use strict";
console.log(arguments)
}
function fn(x, y, z) {
'use strict'
x = 100;
z = 1;
arguments[1] = 10;
console.log(arguments[0], arguments[1], arguments[2]); // 3 10 undefined
console.log(x, y, z); // 100 2 1
}
f(3, 2);
4.7 函数中的length
1 与 arguments 的 length 不太一样,函数的 length 是指函数本身的参数,并非调用时实际传递的参数的个数
Function.length // 函数构造器 ==> 1
console.log((function fn() {}).length); /* 0 */
console.log((function fn(a) {}).length); /* 1 */
console.log((function fn(a, b) {}).length); /* 2 */
console.log((function fn(...args) {}).length); // 0
console.log((function fn(a, b = 1, c) {}).length); // 1
4.8 匿名函数
1 实际上就是把原有的函数名去掉,就变成了没有函数名的函数,简称匿名函数
function fn() {
console.log("普通函数")
}
// 直接运行下面函数是会报错的
function() {
console.log("匿名函数")
}
2 把匿名函数变成自执行函数
(function() {
console.log("自执行函数")
})()
3 可以把匿名函数赋值给一个变量
var fn = function() {}
4 匿名函数在事件中大显身手
var oBox = doxument.getElementById("box")
oBox.onclick = function() {
console.log(this)
}
5 对象方法
var obj = {
name: "ken",
fn: function() {
console.log(this.name)
}
}
6 回调方法
setInterval(function(){
console.log(1)
}, 1000)
setTimeout(function() {
console.log(1)
}, 1000)
7 函数返回值
function fn() {
return function() {
return 1
}
}
4.9 自执行函数,简称 IIFE
1 建议在自执行函数后面加上英文分号 ';',避免打包的时候会被当成函数打包
2 我们常见的一个例子jquery
(function (jq) {
function init() {
console.log('do something');
}
jq.extend({
'myMethod': function () {
init();
}
})
})(jQuery);
5.0 箭头函数
1 箭头函数是没有自己的 this,arguments,super,new操作
2 由于没有 this 所以在使用 call, apply调用函数时,第一个参数通常会被忽略
3 由于没有 this 对象,所以不能进行 new,已经继承中的 super()
let sum = (a,b,c)=>{
return a + b + c
}
sum(1,2,3)
var arr = [1,2,3,4]
arr.forEach((item, index)=>{
console.log(item)
console.log(index)
})
arr.map((item, index)=>{
console.log(item)
console.log(index)
})
arr.filter((index)=>{
return index>2
})
5.1 高阶函数
其实高阶函数也没那么神奇,你可以跟数学类比,就像一个函数的参数可以是另一个函数。JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
1 一个最简单的高阶函数
function addFn(x, y, fn) {
return fn(x) + fn(y)
}
addFn(-5, 6, Math.abs)
2 map 遍历循环
map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
function pow(x){
return x * x
}
var arr = [11,22,33,44,55,66]
arr.map(pow); //[121, 484, 1089, 1936, 3025, 4356]
var arr = [1,2,3,4,6,7]
arr.map(String);// ["1", "2", "3", "4", "6", "7"]
3 reduce 累计叠加
reduce方法有两个参数,第一个参数是一个callback,用于针对数组项的操作;第二个参数则是传入的初始值,这个初始值用于单个数组项的操作。需要注意的是,reduce方法返回值并不是数组,而是形如初始值的经过叠加处理后的操作
var arr = [1,2,3,4,5,6]
arr.reduce(function(x,y) {
return x + y
})
// 或许我们可以自己模拟写一个 reduce 函数
// 我们自己来编写一个属于自己的reduce
function reduce(array,cb){
if(!cb){
return ;
}
var memo = 0;
for(i=0;i<array.length;i++){
memo = memo + array[i];
cb(memo,array[i]);
}
// 返回上一次相加的和 memo
return memo;
}
var sum = reduce(arr,function(memo,num){
console.log(memo,num);
})
console.log(sum);
4 filter 过滤数据
.filter是一个内置的数组迭代方法,它接受一个“谓词(译者注: 指代一个过滤条件的函数)”,该“谓词”针对每个值进行调用,并返回一个符合该条件(“truthy值”)的数组。
// 我们来看看原生的js中的filter
var arr = [1,2,4,5,6,7,8];
var res = arr.filter(function(num){
return num % 2 === 0;
})
console.log(res);
// 我们自己来实现一个
function filter(array,cb){
if(!array) return ;
var res = [];
for( let i=0;i<array.length;i++ ){
if(cb(array[i])){
res.push(array[i]);
}
}
return res;
}
var res = filter(arr,function(num){
return num % 2 === 0;
})
console.log(res);
5 forEach 遍历循环
var arr = [1,2,3,4,5,6]
arr.forEach((item, index)=>{
console.log(item, index)
})
// 首先我们先来自己实现以下一个each函数
function each(obj,fun){
if(!fun)
return;
if(obj instanceof Array){
// 遍历数组的
for(var i=0;i<obj.length;i++){
if(fun.call(obj[i],i)==false)//函数中的this指向obj[i] i为索引
break;
}
}else if(typeof obj === 'object'){
var j = null;
for(j in obj){
if(fun.call(obj[j],j))//函数中的this指向obj[j] j为属性名
break;
}
}
}
// 调用
var arr = ['a','b','c'];
each(arr,function(index){
console.log(index +"=" +this);
});
var obj = {name:'liu',age:12,salary:8000,address:'广州'};
each(obj,function(index){
console.log(index +"=" +this);
});
6 find 查找
find方法对数组中的每一项元素执行一次callback 函数,直至有一个callback返回true。当找到了这样一个元素后,该方法会立即返回这个元素的值,否则返回undefined。注意callback函数只会在分配了值的数组索引上调用,而不会在已删除或未分配值的索引上调用。
调用callback函数带有3个参数:当前元素的值、当前元素的索引,以及数组本身。
如果提供了thisArg参数,那么它将作为每次callback函数执行时的上下文对象,否则上下文对象为undefined。
find方法不会改变数组。
在第一次调用callback函数时会确定元素的索引范围,因此在find方法开始执行之后添加到数组的新元素将不会被callback函数访问到。如果数组中一个尚未被callback函数访问到的元素的值被callback函数所改变,那么当callback函数访问到它时,它的值是将是根据它在数组中的索引所访问到的当前值。被删除的元素不会被访问到。
var arr = [1,2,3,4,5,6]
arr.find((item, index)=>{
return item>2
})
// 当然我们需要一个函数
var arr = [1,2,4,5,6]
function find(array,cb){
if(!array) return ;
for(let [i,k] of array.entries()){
if(cb(k)===true){
return k;
brerak;
}
}
}
var num = find(arr,function(num){
return num % 2 === 0;
})
console.log(num);//2
7 sort 排序
sort() 方法用于对数组的元素进行排序,并返回数组。默认排序顺序是根据字符串UniCode码。因为排序是按照字符串UniCode码的顺序进行排序的,所以首先应该把数组元素都转化成字符串(如有必要),以便进行比较。
语法:arrayObject.sort(sortby);参数sortby 可选,用来规定排序的顺序,但必须是函数。
//要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]
//如果要倒序排序,我们可以把大的数放前面:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]
//默认情况下,对字符串排序,是按照ASCII的大小比较的,现在,排序应该忽略大小写,按照字母序排序。
//要实现这个算法,不必对现有代码大加改动,只要我们能定义出忽略大小写的比较算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']
//忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
//sort()方法会直接对Array进行修改,它返回的结果仍是当前Array:
var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一对象
5.2 闭包
1 概念
闭包是一种特殊的对象。 它由两部分组成:执行上下文(代号A),已经在该执行上下文中创建的函数(代码B)。 当B执行时,如果访问了A中变量对象中值,那么闭包就会产生。 我们只需要知道,一个闭包对象,由A,B共同组成,在以后的文章中,都会将以chrome的标准来称呼。
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
var bar = foo();
bar();
在上面的例子中,首先执行上下文 foo,在foo中定义了函数bar,而后通过对外返回bar的方式让bar得以执行。当时执行时,访问了 foo 内部变量a和b。因此这个时候闭包产生, 在chrome中通过断点调试的方式可以逐步分析该过程,从图中可以看出,此时闭包产生,用foo代替
在图中,箭头所指的正是闭包,其中call stack 为当前的函数调用栈,scope 为当前正在被执行函数的作用域链,Local 为当前活动对象。 现在我们来思考一个小小的问题,把上面的代码调整成这样后,是否形成闭包?
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
bar();
}
foo();
还是形成了闭包。 再来看一个例子,非常有意思的例子
function add(x) {
return function _add(y){
return x+y;
}
}
add(2)(3); //5
这个例子当然是产生闭包的,内部函数_add(y)被调用执行的时候,访问了add函数变量对象的中x的值,这个时候,闭包就会产生,如图所示,一定要记住,函数参数中的变量传递给函数之后也会加到变量对象中
还有一个例子可以验证大家对于闭包的理解。 看看下面这个例子是否产生闭包
var name = "window";
var p = {
name: 'Perter',
getName: function() {
return function () {
return this.name;
}
}
}
var getName = p.getName();
var _name = getName();
console.log(_name);
getName在执行时,他的this其实是指向的是window对象,而这个时候并没有形成闭包的环境,因此并没有产生闭包。
那么这个例子改成如下这样呢?
var name = "window";
var p = {
name: 'Perter',
getName: function() {
return function () {
return this.name;
}
}
}
var getName = p.getName();
var _name = getName.call(p);
console.log(_name);
没有产生闭包, 如果再改成这样呢?
var name = "window";
var p = {
name: 'Perter',
getName: function() {
var self = this;
return function () {
return self.name;
}
}
}
var getName = p.getName();
var _name = getName();
console.log(_name);
这样就会产生闭包了
5.3 垃圾回收机制
垃圾回收机制,知道当一个值失去引用之后就会被标记,然后被垃圾回收机制回收并释放空间。 我们知道,当一个函数的执行上下文运行完毕之后,内部的所有内容都会失去引用而被垃圾回收机制回收。 我们还知道,闭包的本质就是在函数外面保持了内部变量的引用,因此闭包会阻止垃圾回收机制进行回收。 我们用一个例子来证明这一点
function f1() {
var n = 999;
nAdd = function() {
n += 1;
}
return function f2(){
console.log(n);
}
}
var result = f1();
result();
nAdd();
result();
从上面的例子可以看出,因为 nAdd ,f2 都可以访问f1中的n,因此他们都会与f1形成了闭包。这个时候变量n都被保留下来了,因为f2(result)与nAdd执行时间都访问了n,而nAdd每运行一次就会将n加1,所以上例的执行结果非常符合我们的认知。
5.4 作用域与作用域链
先花几秒钟时间来思考一个小问题,闭包会导致函数的作用域链的变化呢?
var fn=null
function foo() {
var a = 2;
function innerFoo() {
console.log(a);
}
fn = innerFoo; // 将innerFoo 的引用赋值给全局变量中的fn;
function bar() {
fn(); //此处保留innerFoo的引用
}
foo();
bar();
}
在这个例子中,foo内部的innerFoo访问了foo的变量a。因此当innerFoo执行时会有闭包产生,这是一个比较简单的例子,不一样的地方是全局变量fn。fn在foo内部获取了innerFoo的引用,并在bar中执行。
那么innerFoo的作用域链会怎么样呢? 在这里需要特别注意的地方是函数调用栈与作用域链的区别。 因为函数调用栈其实是在代码执行时才确定的,而作用域规则在代码编译阶段就已经确定了,虽然作用域链是在代码执行时才生出的,但是他的规则不会在执行期间发生变化。所以闭包的存在不会影响作用域链的变化
5.5 声明周期
我们知道,当一个函数被调用时,一个新的执行上下文就会被创建,一个执行上下文的声明周期大致分为两个阶段:创建阶段和执行阶段。 创建阶段 在这个阶段,执行上下文会分别创建变量对象,确认作用域链,以及确定 this 指向问题 执行阶段 创建阶段之后,就会开始执行代码,这个时候就会完成变量赋值,函数引用,以及执行其他可执行代码
从执行上下文的生命周期可以看到它的重要性,其中涉及变量对象,作用域链,this等许多重要知识点,但是并不是那么容易搞懂的,,这些概念有助于我们真正的理解javascript代码运行的机制。
JavaScript变量的生命周期 最近看国外经典教材的时候发现JavaScript与熟知的Java,C,C++都不同的特性,其中一个就是变量的生命周期
1 在JavaScript中,对于for循环中定义的i变量,其生命周期在循环结束后仍然是有效的。
for (var i=0; i < 10; i++){
doSomething(i);
}
alert(i); //10
这样的特性对于我们传统的习惯来说是不可理解的,这是因为JavaScript变量作用范围没有语句块的概念,他并不像JAVA一样在for循环内部声明的变量,在for循环外部就不能使用。
2.对于全局变量和局部变量的区分,要对var的使用特别注意。
<html>
<head></head>
<body>
<script type="text/javascript">
var global_one = "I am global";
function fun(){
global_two = "I am global too";
var local_one = "I am local";
}
alert(global_one); // "I am global"
//alert(global_two);// error
//alert(local_one);//error
fun();
alert(global_one); // "I am global"
alert(global_two);// "I am global"
//alert(local_one);//error
</script>
</body>
</html>
从上面的实例可以看到两点: 第一,JavaScript中的方法内部定义变量的时候如果没有加var,就是全局变量;否则为局部变量; 第二,当fun()没有执行的时候,方法内部的全局变量是不会声明并且定义的。
5.6 函数颗粒化
我们还是从一道面试题说起吧,如下要求
我们想要创建一个函数,在连续调用时将数字加在一起。
add(1)(2);
// returns 3
我们还希望能够继续为函数添加数字。
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10
add(1)(2)(3)(4)(5); // 15
等等。
单个调用应该返回传入的数字。
add(1); // 1
我们应该能够存储返回的值并重用它们。
var addTwo = add(2);
addTwo; // 2
addTwo + 5; // 7
addTwo(3); // 5
addTwo(3)(5); // 10
我们可以假设传入的任何数字都是有效的整数。
其实如果仔细研究我们就会发现这道题实际上应用的是不定参数,还有一个就是改造 valueOf()和toString()这两个方法。
当函数直接参与常量计算的时候,函数会默认调用toString(),函数会被转换为字符串参与计算,我们来看个例子
function fn() { return 50 }
console.log(fn + 60);
但是实际上我们可以覆写toString()方法,来改变上面这道题的答案
除此之外,当我们重写函数的 valueOf方法时也能改变函数的隐式转换效果。
当我们同时重写函数的toString和valueOf方法时,最终的结果会取 valueOf方法的返回结果。
补充了上面之后就可以来尝试之前的题目了,add方法的实现仍然是一个参数的收集过程,当add函数执行到最后时,返回的仍然是一个函数,但是我们可以通过定义toString/valueOf的方式,让这两个函数可以直接参与计算,并且转换的结果是我们想要的结果,而且他本身也仍然可以继续收集新的参数,实现方式如下
function add() {
// 第一次执行时,定义一个数组专门yo拿过来存储所有参数
var _args = [].slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var adder = function() {
var _adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return _adder;
}
// 利用隐式转换的特性,当最后执行时隐式转换,计算最终的值并返回
_adder.toString = function() {
return _args.reduce(function(a,b){
return a + b;
})
}
return _adder;
}
return adder(..._args);
// return adder.apply(null, _args);
}
var a = add(1)(2)(3)(4);
var b = add(1,2 ,3, 4);
var c = add(1,2)(3,4);
var d = add(1,2,3)(4);
console.log(a+10);
console.log(b+20);
console.log(c+30);
console.log(d+40);
console.log(a(10) + 100);
console.log(b(10) + 100);
console.log(c(10) + 100);
console.log(d(10) + 100);
function fn() { return 20 }
fn.valueOf = function() { return 60 };
console.log(fn + 10); // 70
function fn(){ return 20 };
fn.toString = function() { return 30 };
console.log(fn + 10);