函数
概念:
函数就是具备某个功能的一个工具。是完成某个功能的一段代码。
大家以前有没有用过函数呀?
parseInt() alert() 这都是函数,是系统提供的,直接拿来就能用。
系统提供了很多函数,但是并不能包含所有的功能,所以有些功能需要我们自己来写----自定义函数。函数定义好以后,就可以像系统函数一样使用了,不用再重复写了。
所以经常写的代码,就写一个函数,需要的时候调一下好了。
定义语法:
function 函数名(){
代码段
}
# function是一个关键字,函数名自定义,定义规则和变量的定义规则相同
当我们定义好函数,并在页面中刷新的时候,会发现这段代码并没有被执行。因为函数定义好后,是不会自动执行的,需要我们进行调用。
函数的调用:
语法:
函数名()
调用的语法很简单,这样我们以后还需要执行函数中的代码的时候,就不用再重写那么多的代码了,只需要简单的将原来定义好的函数进行调用即可。
// 定义函数:求两个数的和
function fn(){
var a = 1;
var b = 2;
var c = a + b;
console.log(c);
}
// 函数定义好以后,不会自动执行,需要手动调用
fn();
带参数的函数:
函数代码中会发生改变的值用变量来代替,入口是声明函数时的小括号
function zizeng(a){ // 叫做形参 - 形式上的参数
var b = a + 1;
console.log(b);
}
调用函数的时候,需要给参数赋值
zizeng(1); // 实参 - 实际上的参数,实参其实就是给形参赋值
声明函数时候带进去的那个参数叫形参
调用函数的时候给形参进行赋值的实际值是实参
函数的本质
当我们去调用函数的时候,通过函数的名称就可以调用到,那说明我们在定义函数的时候,函数就已经保存起来了,所以下面才能调用出来。
函数定义在内存中的体现:
function fn(){
console.log(11)
}
这段代码,在内存创建了一个空间,名字叫fn,这个空间中存储的数据是函数这整段代码。
调用函数,就相当于将这段代码拿出来执行。
匿名函数
既然函数的定义跟变量的定义过程差不多,那函数的定义就可以像变量一样进行。
var f = function fn(){
console.log(12)
}
这是定义一个变量,将函数代码放到变量空间中,这样的函数也是可以正常进行调用的,就使用变量的名称就行:
f() // 12
那fn这个函数的名字还能进行调用吗:
fn() // fn is not defined
这就说明,当将一个函数赋值给一个变量的时候,这个函数的名字就没有用了,所以我们可以将这个函数名称省略:
var f = function(){ console.log(12) }
这样还是可以正常调用的:
f()
这种没有名字的函数就叫做匿名函数。
匿名函数不能单独存在,会报错:
function(){ console.log(13) }
除非将这个函数用小括号括起来:
(function(){ console.log(13) })
但是这种没有名字的函数就无法调用了,js提供了一个专门用来调用匿名函数的语法:
(function(){ console.log(13) })()
后面加小括号就表示调用,这种定义并调用函数的语法,叫做自调用函数。即,函数定义好立即调用。
自调用函数也可以不给函数加小括号,在函数前加感叹号或波浪线:
!function(){ console.log(14); }() ~function(){ console.log(14); }()
同样是立即执行的函数。
这种函数也是可以传参数的:
(function(a,b){ var c = a + b; document.write(c); })(1,2);
函数的优点:
-
实现了代码的可重用性
-
实现了模块化编程
我们在使用工具的时候,很多工具需要我们带东西进去,比如洗衣服要放衣服进去,用函数这个工具来说的话,就是需要带参数。
带返回值的函数
之前的函数,在调用后,就是将函数中的代码执行了,没有得到一个结果,如果我们希望函数调用后得到一个结果,在后续的代码中,需要用到这个结果,例:
// 求三门成绩的和 function add(ch,math,en){ var sum = ch + math + en; console.log(sum) } // 根据函数中求出的和,计算平均数 var avg = add(20,30,40)
上面的函数是没有办法实现的,此时,需要使用函数的返回。
不是所有的程序的结果都需要输出在页面中,有时候,我们只是想让这一段代码得出一个结果,后续代码得到这个结果后进行后续处理。那么上面的函数显然已经不适用了。我们需要使用函数的返回。
函数返回结果,在函数中使用return关键字,后面跟要得到的结果。
function add(ch,math,en){ var sum = ch + math + en; return sum }
此时调用函数,就得到一个结果,可以将这个结果赋值给变量或进行下一步操作。
function add(ch,math,en){ var sum = ch + math + en; return sum } var a = add(20,30,40) var avg = add(20,30,40)/3
return关键字除了可以给函数调用返回结果,还可以结束函数运行:
function add(ch,math,en){ var sum = ch + math + en; return sum console.log(sum) } add(20,30,40)
我们发现,调用函数后,函数中的输出代码没有执行,也就是return将函数结束了。
return在返回结果的时候,只能返回一个结果,不能返回多个:
function add(ch,math,en){ var sum1 = ch + math var sum2 = ch + math + en return sum1,sum2 } var res = add(20,30,40) console.log(res) // 90
调用后的res值得到一个90的结果,就说明return只能得到一个结果,不能得到多个。
return总结:
-
终止代码继续运行
-
函数运行后返回一个结果,只能返回一个
预解析
console.log(a); // 因为变量a没有声名过,所以会报错 test(); // 函数未定义,所以报错
所以在正常情况下,变量要使用或函数要调用,都需要提前定义变量或函数。
var a = 10; console.log(a) function test(){ console.log("this is test function") } test()
但我们发现一件比较有意思的事情:先输出变量,然后再定义变量,浏览器不报错;先调用函数,再定义函数,不报错,函数能调用
console.log(a) var a = 10; test() function test(){ console.log("this is test function") }
原因是浏览器执行js代码之前,会有一个预解析的过程:
浏览器中有一段程序专门用来解析js代码, 叫做js解析器。js解析器在执行js代码的时候,分两步进行:
-
预解析js代码
预解析的过程,就是查找代码中的var和function这两个关键字,找到以后,将变量和函数提前存到内存中,并给他们赋一个初始值,变量的初始值为undefined,函数的初始值为代码段。
-
开始按顺序一行一行解读代码
解读代码的时候,会略过变量和函数的定义,因为变量和函数的定义已经提前放在内存中了,提前储存的变量和函数的值会随着代码的解读而发生变化,也就是变量的赋值和函数的调用。
预解析分为变量的预解析和函数的预解析,也就是代码在执行之前先进行解析,将变量和函数的定义放在内存中。
但是在打印之后声名过变量的话,情况就不一样了。
console.log(a) var a = 10; test() function test(){ console.log("this is test function") }
代码执行之前先预解析:
// 先将变量和函数的定义放在内存中 var a function test(){ console.log("this is test function") } // 下面就忽略掉定义的过程 console.log(a) a = 10; test()
开始按照预解析后顺序执行:
var a function test(){ console.log("this is test function") } console.log(a) // 前面有定义过变量a,没有赋值,所以变量的值为undefined a = 10; // 将a的值改变为10 test() // 前面有定义过函数,内存中能找到,所以调用成功
面试题:
// 第1题 console.log(num) var num = 100 // 第2题 fn(); function fn() { console.log(123); } // 第3题 console.log(fn) fn() var fn = function () { console.log(123); } // 第4题 fun() var fn = function () { console.log('我是 fn 函数') } function fun() { console.log('我是 fun 函数') } fn() fn = 100 fn() // 第5题 fn() function fn() { console.log('我是一个 fn 函数') } fn() var fn = 100 fn() // 第6题 var fun = 200 fun() var fun = function () { console.log('我是一个 fun 函数') } fun() // 第7题 var a = b a = 0 b = 0 console.log(a) console.log(b) // 第8题 console.log(num) if (false) { var num = 100 }
预解析总结:
-
匿名函数赋值给变量的定义方式,预解析时遵循变量的预解析规则,不会将函数代码预解析
-
预解析的时候,会将定义提前放在内存中,不会提前将赋值放在内存中
-
如果变量名和函数名同名了,保留函数预解析,忽略变量预解析
因为函数预解析其实包含了赋值的过程,函数定义放在内存中的时候将函数的代码也放在内存中
变量的预解析只有空间,没有值,所以如果是先预解析变量,那后面的函数预解析赋值就将空间中放入了值,如果是先预解析的函数,再次预解析变量的时候,空间已经存在了,再次定义空间也是没有意义的。
-
省略var定义的变量是不会有预解析的
-
js代码如果报错了,那后面的代码就不会执行了
-
不会执行的代码中有变量或函数定义也会预解析,因为预解析在执行之前。
函数的嵌套
函数结构中的大括号,里面放的是代码段,既然是代码段,就可以写判断、循环甚至函数代码,这样就形成了函数的嵌套。
函数的大括号中可以写函数的定义,可以写函数的调用:
function fn(){ console.log(1) function fun(){ console.log(2) } fun() } fn() function fn(){ console.log(1) } function fun(){ fn() console.log(2) } fun()
函数的调试
js的写法
js跟css一样,script标签可以放在网页的任何位置,script标签也可以写多个:
<body> </body> <script> console.log(1); </script> <script> console.log(2); </script>
多个标签之间的变量也是可以互相使用的:
<script> var a = 10 </script> <script> console.log(a); </script>
但这种情况必须上面定义,下面输出,不能在上面输出,下面定义。
因为,js代码的预解析,只能在当前这个script标签中,不能对整个html生效。
作用域
能起到作用的区域就叫做作用域。定义在不同区域的变量,他的作用域是不一样的。
不在任何一个函数中定义的变量叫全局变量。他的作用域是定义之后的所有文档区域。
例:
<script> var a = 10 </script> <script> console.log(a); // 后面的script标签能用 function fn(){ console.log(a); // 后面的函数中能用 } fn() </script>
在函数中定义的变量叫局部变量。他的作用域是当前这个函数中,函数外不能使用。 例:
function fn(){ var a = 10 } fn() console.log(a); // 报错:a is not defined
如果全局和局部都有同名的变量,局部输出时如何输出呢?
var a = 10; function fn(){ var a = 20; console.log(a) // 20 } fn()
变量在输出时,首先考虑当前作用域,当前作用域中就会输出当前作用域中的变量,如果当前作用域中没有,才会考虑外面的全局。
作用域链
函数是写在全局中,也就是说,局部作用域是被嵌套全局作用域中的。函数中也是可以定义函数的,也就是局部作用域中可以再有局部作用域,这样就形成了作用域的嵌套,我们将这个链式结构叫做作用域链。
例:
var a = 10; function fn(){ var b = 20; function fun(){ var c = 30; } fun() } fn()
这段代码的作用域链如下:
作用域链有助于我们分析变量的使用以及赋值规则,规则如下:
-
当使用变量(将变量当做值赋值,输出变量,使用变量计算)的时候,先在当前作用域中找是否定义过变量,如果定义过,就使用;如果没有定义过,就去上一级作用域中找是否定义过,定义过就使用,没有定义就继续去上级作用域中找,......直到找到全局,全局中如果定义过,使用的就是全局中的变量;如果全局中没有定义过,报错:XXX is not defined
-
当给一个变量赋值的时候,先在当前作用域中找是否定义过这个变量,如果定义过,就给这个变量赋值;如果没有定义过,就去上级作用域中找是否定义过,找到就赋值,没找到就继续去上级作用域中找,......直到找到全局,全局中定义过,就给这个全局变量赋值,如果全局没有定义过,就在全局定义这个变量并赋值
练习:
function fn() { console.log(num) return var num = 100 } fn() var num = 10; fn1(); function fn1() { console.log(num); var num = 20; } var a = 18; fn2(); function fn2() { var b = 9; console.log(a); console.log(b); } fn3(); console.log(c); console.log(b); console.log(a); function fn3() { var a = b = c = 9; console.log(a); console.log(b); console.log(c); } var a = 4; console.log(a); a = 6; console.log(a); function a() { console.log('哈'); } a(); a = 10; console.log(a); function fn(a) { console.log('我是 fn 函数') a() function a() { console.log('我是函数 a') } } fn(10)
结论:
-
全局中有预解析,局部中也有预解析,局部的预解析,只能在局部中进行,不会将变量提升到全局
-
使用连等的方式定义变量并赋值,只有第一个有定义过程,其余的都是直接赋值
-
函数定义好以后,函数名就跟变量名一样,可以使用函数名修改这个空间中的值
-
局部的预解析会在形参赋值之后,预解析中的函数会覆盖掉形参赋的值
递归函数
递归函数就是在函数中调用自己。
// 求10的阶和,即:10+9+8+...+1 function facSum(num){ if(num == 1){ return 1; } return num + facSum(num-1); } var res = facSum(10); console.log(res); // 55
事件
js中的事件指的是用户在网页中的行为,例如:鼠标点击、鼠标移动、。。。。。。
事件通常由3个要素组成:
-
事件源:触发事件的标签元素,例如,点击的是div、还是button 。。。
-
事件类型:行为的类型,是单击还是双击,还是右击。。。
-
事件处理程序:事件触发后要做的事情 - 函数
例:
<button id="btn">按钮</button> <script type="text/javascript"> btn.onclick = function(){ alert("点击了按钮!"); } </script>
事件类型
事件 | 备注 |
---|---|
onclick | 当鼠标左键单击 |
ondblclick | 当鼠标左键双击 |
onmouseover | 当光标在指定的内容上面 |
onmouseout | 当光标离开指定的内容 |
onkeydown | 当键盘按下的那一刻 |
onkeyup | 当键盘抬起的那一刻 |
onfocus | 当输入框得到焦点 |
onblur | 当输入框失去焦点 |
onchange | 当指定的标签里面内容有变化的时候 |
onsubmit | 提交事件 |
onload | 当整个网页加载完成后 |
事件的其他写法:
// 给事件赋值函数名称 btn.onclick = fn function fn(){ console.log('点击了按钮') } // 给事件赋值变量名 - 值是函数 var fn = function(){ console.log('点击了按钮') }
注意:如果给事件赋值函数名,千万不能加小括号调用
事件还可以写在行内:
<!-- 直接在行内写js代码 --> <button οnclick="console.log('点击了按钮')">按钮</button> <!-- 可以在行内调用函数 --> <button οnclick="fn()">按钮</button> <script> function fn(){ console.log('点击了按钮') } </script>
注意:在行内的事件中调用函数一定要加小括号调用才行
js可以像css一样有3种写法:
-
行内写法:事件
-
内联写法:平常写的代码
-
外联写法:项目中要将js代码放在一个文件中,在html中引入js文件
<script src="js文件路径"></script>
对象
如果我们存储一个人信息的姓名和年龄,需要两个变量:
var name = '张三' var age = 12
如果存储另一个人的信息,需要再次定义两个变量,但是变量名不能重复,因为重复会覆盖掉上面的变量:
var name1 = '李四' var age1 = 13
从上面4个变量,可以看出来,每个变量都是独立的,互相之间是没有联系的,如果要将多个变量组合在一起去描述一个人的信息,变量之间没有关系是很容易弄错的。
为了让多个值之间有联系,确保多个值就是用来描述一个人的,就需要对象这种数据来处理。
在学习数据类型的时候,学习过对象这种数据类型,他的表现形式:
var arr = []; var obj = {}; var none = null;
这三种不同的表现形式指的都是对象。我们让多个不同的值描述一个人,重点看使用{}
定义的对象。
定义对象
var obj = {};
这样定义的对象是空数据,其中什么也没有。对象中的值,是由键值对组成。
键值对是指在描述一个事物的时候,需要一个名字,对应一个值,例如:
姓名:张三 // 姓名是键,张三是值 width:100px // width是键,100px是值 border=1 // border是键,1是值
定义有数据的对象,键和值之间使用冒号隔开,键值对之间使用逗号隔开:
var obj = { name:"张三", age:12 }
对象有一个特性,第一次打开的时候,只能看到Object,再次刷新页面, 会显示里面的数据:
对象中键值对的数量没有限制,可以有任意多个:
var obj = { name:"张三", age:12, height:180, weight:150 };
对象中的键都是字符串,只是正常情况下可以省略引号,但如果键中包含连字符,就不能省略引号了:
var obj = { "name":"张三", age:12, height:180, "province-name":"山东省" }; console.log(obj);
从输出的结果中能看到,书写顺序和显示的顺序是不同的,因为对象中的键值对是没有顺序的。
对象的基本操作
访问对象中的值:
对象.键 # 这种方式的属性名不用加引号 # 或 对象[键] # 这种方式的属性名必须加引号
例:
var obj = { name:"张三", age:12 }; console.log(obj.name); console.log(obj["age"]);
对象的遍历:
遍历的意思就是将每一个值都访问一遍。
js提供了专门用来遍历对象的操作语法:
for(var attr in obj){ # 这里的attr代表对象属性名 # obj表示这个对象 }
例:
var obj = { name:"张三", age:12, height:180 }; for(var i in obj){ // 这里的i表示对象的属性名,是一个字符串 console.log(i,obj[i]); }
注意:遍历对象时,输出对象中的值,必须使用对象[代表键的变量]
这种形式来输出。
因为,如果使用.
来访问的话,是有歧义的:
var obj = { name:"张三", age:12, height:180 }; // 输出张三 console.log( obj.name );
如果使用一个变量来代替name
这个键:
var obj = { name:"张三", age:12, height:180 }; // 输出张三 console.log( obj.name ); var a = 'name' console.log( obj.a ); // undefined
此时,浏览器会将obj.a
识别为:我们正在访问obj中键为a
的值,.
后面的内容,表示对象中的键的名称,所以此时只能使用[]
的形式来访问:
var obj = { name:"张三", age:12, height:180 }; // 输出张三 console.log( obj.name ); var a = 'name' console.log( obj[a] ); // 张三
此时[]
中的a
是一个变量,因为没有加引号,代表['name']
。
给对象添加属性:
var obj = {}; console.log(obj); obj.name = '李四'; obj["age"] = 12; console.log(obj);
打印对象的结果:
访问结果 |
---|
如果设置的属性名是对象中已经存在的呢?
var obj = { name:"张三", age:12 } console.log(obj); obj.age = 15; console.log(obj);
打印结果:
访问结果 |
---|
删除对象中的键值对:
delete 对象.键 delete 对象[键]
例:
var obj = { name:"张三", age:12, height:180 }; console.log(obj); delete obj.name; console.log(obj); delete obj['age'] console.log(obj);
方法概念
对象中值的类型是没有限制的,可以是任意类型。当值不是函数的时候,我们将这个键值对叫做对象的属性,当值是一个函数的时候,我们将这个键值对叫做对象的方法。
var obj2 = { name:'王五', study:function(){ console.log("在学习"); } } console.log(obj2);
name
就是对象obj2
的属性,study
就是对象obj2
方法
方法的访问和属性是一样的:
console.log(obj2.study) obj2.study(); // 因为函数执行需要调用,所以需要加小括号
构造函数
对象除了上面直接使用{}
的形式来创建,还可以使用函数来创建,任意一个函数在调用的前面加new
关键字,就可以得到一个对象。例:
function fn(){} var obj = new fn() console.log(obj)
当一个函数被用来创建对象使用的时候,这个函数就叫做构造函数。
上面的函数fn
需要自己创建,js系统为了方便创建对象,提供了一个专门用来创建对象的函数:Object
var obj = new Object()
我们创建对象也可以使用这种方式去创建。其中的Object
函数就是js系统提供的构造函数。
使用构造函数创建对象并给对象添加键值对:
var obj = new Object({ name:"张三", age:12 }) console.log(obj);