一、函数定义
1、函数声明
JavaScript 函数通过 function 关键词进行定义,其后是函数名和括号 ()。注意,如果函数重名的话,后面定义的函数会覆盖前面定义的函数,不管在哪里调用这个函数,调用的都是后面定义的函数。
- 形参:定义函数参数的名称、类型(Js里是弱类型,不用声明)等。
- 实参:实际参数,就是在运行时真正传入函数中的参数。
function methodName(参数 1, 参数 2...) {
要执行的代码;
}
- 示例代码:
<script>
function sum(a,b){
console.log(a + b);
}
// 后面声明的sum()覆盖了前面声明的
sum(10, 10); // 30
function sum(a,b){
console.log(a + a + b);
}
sum(10, 10); // 30
</script>
2、函数表达式
函数表达式(function expression)非常类似于函数声明(function statement),并且两者拥有几乎相同的语法。函数也是一种数据类型——function,所以可以将它赋值给变量。
-
函数表达式与函数声明的最主要区别是函数名称(function name),在函数表达式中可省略它,从而创建匿名函数(anonymous functions)。
-
一个函数表达式可以被用作一个IIFE(Immediately Invoked Function Expression,即时调用的函数表达式),它一旦定义就运行。
-
示例代码:
<script>
// 函数表达式声明了一个匿名函数,并赋给了变量func
var func = function(){console.log("hello world1")};
// 由这个变量调用函数
func();
// 不用将函数表达式赋给变量,函数自调用
(function(){console.log("hello world2")})();
console.log(typeof func); // function
</script>
二、函数返回值
每一个函数都会有一个返回值,这个返回值可以通过关键字“return”进行设置。若未显式地设置函数的返回值,那函数会默认返回一个undefined值。
<script>
// 函数有返回值
function f1(x,y){
return x + y;
}
// 函数无返回值
function f2(x, y){
var sum = x + y;
}
// 函数无明确返回值
function f3(){
return;
}
console.log(f1(10,10)); // 20
// 可以只传部分参数,未传入的参数默认为 undefined
console.log(f1(10)); // NaN
console.log(f2(10,10)); // undefined
console.log(f3()); // undefined
</script>
- 函数作为返回值
<script>
function f1(){
console.log("f1调用了");
return function(){console.log("返回的函数调用了")}
}
(f1())();
</script>
- 输出结果:
三、函数属性
1、Function.arguments
以数组形式获取传入函数的所有参数,即传入的实参。
arguments 变量不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个对象(Object)。因此,无法对 arguments 变量使用标准的数组方法。
- 示例代码:
<script>
function f(){
console.log(arguments.length);
}
f(1, 2, 3); // 3
</script>
2、Function.name
获取函数的名称。
3、Function.length
获取函数的接收参数个数,即形参个数。
4、Function.caller
获取调用这个函数的具体对象。
四、函数高阶
所有的函数实际上都是通过Function的构造函数创建出来的实例对象,即函数也是对象,而Function对象里的原型( __proto__ )属性最终指向的也是Object的原型(Prototype)。
<script>
var f1 = function(num1, num2){
return num1 + num2;
}
var f2 = new Function("num1", "num2", "return num1+num2");
console.log(f1(10,20)); // 30
console.log(f2(10,20)); // 30
// f1的原型等于Function的原型,说明f1是通过Function的构造函数生成的实例对象
console.log(f1.__proto__ == Function.prototype); // true
console.dir(Function);
</script>
1、函数对象的方法
(1)Function.prototype.apply()
apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数,可以在函数调用时改变 this 的指向。
func.apply(thisArg, [argsArray])
- thisArg:可选的。在 func 函数运行时使用的 this 值。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象(window),原始值会被包装。
- argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。
- 返回值:调用有指定this值和参数的函数的结果。
示例代码:用 apply 将数组添加到另一个数组
var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]
(2)Function.prototype.call()
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
fun.call(thisArg, arg1, arg2, ...)
- thisArg:可选的。在 func 函数运行时使用的 this 值。
- arg1, arg2, …:可选的。指定的参数列表。
- 返回值:调用有指定 this 值和参数的函数的结果。
示例代码:使用 call 方法调用函数并且不指定第一个参数
var sData = 'Wisen';
function display() {
console.log('sData value is %s ', this.sData);
}
display.call(); // sData value is Wisen
(3)Function.prototype.bind()
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的 this 被bind()的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。
function.bind(thisArg[,arg1[,arg2[, ...]]])
- thisArg:调用绑定函数时作为this参数传递给目标函数的值。
- arg1, arg2, …:当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
- 返回值:返回一个原函数的拷贝,并拥有指定的this值和初始参数。
示例代码:创建绑定函数
bind() 最简单的用法是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。
this.x = 9; // 在浏览器中,this指向全局的 "window" 对象
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 81 - this指向 module 对象
var retrieveX = module.getX;
retrieveX(); // 9 - this指向 window
// 创建一个新函数,把 'this' 绑定到 module 对象
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81 - this指向 module 对象
2、函数作为参数
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
- 示例代码:
<script>
// f2为匿名回调函数,作为f1的参数
function f1(f2 = function(){console.log("f2调用了")}){
f2();
}
f1(); // 输出"f2调用了"
</script>
3、作用域
(1)局部作用域
变量在函数内使用 var 声明,则变量的作用域为局部作用域,称这种变量为局部变量,只能在函数内部访问。局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。
<script>
function f(){
var num = 100;
console.log("函数内的num:" + num); // 100
}
f();
console.log("函数外的num:" + num); // 报错
</script>
(2)全局作用域
变量在函数外定义,即为全局变量,变量的作用域为全局作用域,即网页中所有脚本和函数均可使用,全局变量在页面关闭后销毁。在JavaScript中,如果一个变量声明时,没有使用 var ,称这个变量为隐式全局变量,其作用域也为全局作用域。
全局变量与隐式全局变量的主要区别在于,隐式全局变量可以使用 delete 关键字进行删除,即从内存中释放,而全局变量只能等待页面关闭后,才能从内存中释放。
<script>
var num = 10;
</script>
<script>
console.log("全局变量num的值:" + num); // 10
function f(){
num2 = 20;
}
// 要执行函数之后,隐式全局变量才会声明
f();
console.log("函数中声明的隐式全局变量的值:" + num2); // 20
delete num;
delete num2;
console.log(num); // 10
console.log(num2); // 报错
</script>
(3)作用域链
当解析一个变量时,首先会在当前作用域寻找,如果当前作用域找不到,才会往外层作用域继续寻找。
<script>
var num = 10;
(function(){
var num = 20;
(function(){
var num = 30;
console.log(num); // 30
})();
})();
</script>
4、预解析
JavaScript代码的执行是由浏览器中的JavaScript解析器来执行,分为预解析过程和代码执行过程。预解析过程如下:
- 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值;
- 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用;
- 先提升var,再提升function;
示例代码1:
<script>
console.log(num); // 报错,因为声明提前只提升到当前作用域
</script>
<script>
// 变量声明提前
console.log(num); // undefined
var num = 10;
console.log(num); // 10
// 函数声明提前
f(); // 函数f调用
function f(){
console.log("函数f调用");
}
</script>
<script>
console.log(num); // 10
</script>
示例代码2:
<script>
// 先提升变量var,再提升function,所以这里输出的是函数a
console.log(a); // f a(){}
var a = 10;
function a(){}
</script>
示例代码3:
<script>
f();
console.log(c); // 10
console.log(b); // 10
console.log(a); // 报错
function f(){
console.log(a); // undefined
console.log(b); // 报错
console.log(c); // 报错
// 这种声明方式下,a为局部变量,b、c为隐式全局变量
var a = b = c = 10;
}
</script>
示例代码4:
<script>
f(); // 报错,因为 var f; 声明提升了,此时f是 undefined
var f = function(){console.log("函数f被调用了!")};
</script>
5、闭包
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。
在JavaScript中,函数在每次创建时生成闭包。闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。
示例代码:
在内部函数中保留了对外部变量num的引用,所以虽然num是局部变量,但并没有被释放,但是显然,我们无法像访问全局变量那样直接访问num变量。
<script>
function f(){
var num = 10;
// 内部函数保留对变量num的引用
return function(){return ++num;};
}
var getNum = f();
console.log(getNum()); // 11 - num = 10 + 1 = 11
console.log(getNum()); // 12 - num = 11 + 1 = 12
console.log(num); // 报错
</script>
6、沙箱
沙箱(Sandbox),是一种虚拟的程序运行环境,用以隔离可疑软件中的病毒或者对计算机有害的行为。JavaScript变量的作用域是函数体,因此,利用函数体将执行环境包裹起来便成了实现Sandbox的一种可行方案。
- 示例代码:
<script>
// 原环境变量
var num = 10;
// 匿名函数内构成沙箱
(function(){
// 可能有害的代码
var num = 20;
num++;
})();
console.log(num); // 10
</script>
7、深拷贝与浅拷贝
基本数据类型(undefined,boolean,number,string,null),是存放在栈内存中的简单数据段。在进行赋值操作时,由于是传值,所以它的原始值是不会改变的,而是在内存中新开辟一段栈内存,然后再将值赋值到新的栈中。
<script>
var str = "abc";
// 尝试更改 str 的值
str[1] = "f";
// 动态修改了基本数据类型的值,它的原始值也是不会改变的
console.log(str); // abc
</script>
引用类型(object),是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。在进行赋值操作时,由于是传址,只是改变指针的指向,所以两个变量(指针)指向的是同一个对象,操作的也当然是同一个对象。
var obj = {name : "Tom"};
// 传址
var obj2 = obj;
// 实际上是在操作 obj 对象
obj2.name = "Bob";
console.log(obj.name); // Bob
(1)浅拷贝
浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据,即引用类型数据进行的是引用复制。
<script>
var obj1 = {
id : "001",
position : {
country : "china",
city : "ShangHai"
}
}
var obj2 = {};
// 遍历对象属性,直接赋值
for(var key in obj1){
obj2[key] = obj1[key];
}
// 改变引用属性的值,改变了源对象的值
obj2.position.city = "Beijing";
console.log(obj1.position.city); // Beijing
</script>
(2)深拷贝
深拷贝是对对象以及对象的所有子对象进行递归拷贝,且对于子对象的拷贝,会开辟一块新空间进行复制。
<script>
var obj1 = {
id : "001",
position : {
country : "china",
city : "ShangHai"
}
}
var obj2 = {};
function copy(obj1, obj2){
// 遍历对象属性,直接赋值
for(var key in obj1){
var item = obj1[key];
if(item instanceof Array){
// 如果属性为数组
obj2[key] = [];
copy(item, obj2[key]);
}else if(item instanceof Object){
// 如果属性为对象
obj2[key] = {};
copy(item, obj2[key]);
}else{
// 如果属性为基本类型
obj2[key] = item;
}
}
}
// 进行深拷贝
copy(obj1, obj2);
// 改变引用属性的值,不会改变原对象的引用属性的值
obj2.position.city = "ShangHai";
console.log(obj1.position.city); // Beijing
</script>
8、正则表达式
正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。
(1)创建正则表达式
- 使用正则表达式字面量
var re = /ab+c/;
使用一个正则表达式字面量,其由包含在斜杠之间的模式组成,这种方式为正则表达式提供了脚本加载后的编译。当正则表达式保持不变时,使用此方法可获得更好的性能。
- 调用RegExp对象的构造函数
var re = new RegExp("ab+c");
使用构造函数为正则表达式提供了运行时的编译。使用构造函数的方式,当你知道正则表达式的模式将会改变,或者你不知道模式,并且从其他来源获取它,如用户输入。
(2)使用正则表达式
正则表达式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、matchAll、replace、search 和 split 方法。
方法 | 描述 |
---|---|
exec | 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。 |
test | 一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。 |
match | 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。 |
matchAll | 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。 |
search | 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。 |
replace | 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。 |
split | 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。 |
(3)通过标志进行高级搜索
正则表达式有四个可选参数进行全局和不分大小写搜索。这些参数既可以单独使用也可以一起使用在任何顺序和包含正则表达式的部分中。
标志 | 描述 |
---|---|
g | 全局搜索 |
i | 不区分大小写搜索 |
m | 多行搜索 |
s | 允许 . 匹配换行符 |
u | 使用unicode码的模式进行匹配 |
y | 执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标志 |
var re = /pattern/flags;
// 或者
var re = new RegExp("pattern", "flags");
(4)测试案例
- 案例1:验证密码的强度(弱、中、强)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#dv {
margin: 200px 400px;
}
.strengthLevel0 {
height: 6px;
width: 120px;
border: 1px solid #ccc;
padding: 2px;
}
.strengthLevel1 {
background-color: red;
height: 6px;
width: 40px;
border: 1px solid #ccc;
padding: 2px;
}
.strengthLevel2 {
background-color: orange;
height: 6px;
width: 80px;
border: 1px solid #ccc;
padding: 2px;
}
.strengthLevel3 {
background-color: green;
height: 6px;
width: 120px;
border: 1px solid #ccc;
padding: 2px;
}
</style>
</head>
<body>
<div id="dv">
<label for="pwd">密码</label>
<input type="text" id="pwd" maxlength="16"><br/>
<div>
<em>密码强度:</em>
<em id="strength"></em>
<div id="strengthLevel" class="strengthLevel0"></div>
</div>
</div>
<script>
// 获取输入密码的强度级别
function getLevel(pwd){
var lev = 0;
// 密码中是否有数字
if(/[0-9]/.test(pwd)){
lev++;
}
// 密码中是否有字母
if(/[a-zA-Z]/.test(pwd)){
lev++;
}
// 密码中是否有特殊字符
if(/[^0-9a-zA-Z_]/.test(pwd)){
lev++;
}
return lev;
}
// 注册输入框的键盘抬起事件
document.getElementById("pwd").onkeyup = function(){
// 根据密码强度级别,设置样式,密码需不少于6位
document.getElementById("strengthLevel").className = "strengthLevel" +
(this.value.length >= 6? getLevel(this.value) : 0);
}
</script>
</body>
</html>
- 案例2:测试通过标志 g 和 i ,进行高级搜索
var str = "abcdacbdAb";
// 默认情况下,返回匹配到的第一个字符串信息构成的数组
console.log(str.match(/a[a-z]/)); // ["ab", index: 0, input: "abcdacbdAb", groups: undefined]
// g表示全局模式,返回所有匹配到的字符串构成的数组
console.log(str.match(/a[a-z]/g)); // ["ab", "ac"]
// i表示不区分大小写
console.log(str.match(/a[a-z]/ig)); // ["ab", "ac", "Ab"]
- 案例3:测试 String 的 match() 和 RegExp 的 exec() 区别
var str = "abcdacbdAb";
// 创建一个RegExp对象
var reg = /a[a-z]/g;
// 匹配到的字符串数组 ["ab", "ac"]
console.log(str.match(reg));
// 匹配到的第一个字符串 ["ab", index: 0, input: "abcdacbdAb", groups: undefined]
console.log(reg.exec(str));
// 匹配到的第二个字符串 ["ac", index: 4, input: "abcdacbdAb", groups: undefined]
console.log(reg.exec(str));
// 匹配不到 null
console.log(reg.exec(str));
// 匹配到的第一个字符串 ["ab", index: 0, input: "abcdacbdAb", groups: undefined]
console.log(reg.exec(str));
在全局模式下,match() 返回匹配到的字符串构成的数组,而 exec() 每次会返回下一个找到的字符串信息构成的数组,直到找不到时返回null,之后再调用时又会从头开始寻找。显然,这里需要一个 RegExp 对象保存执行信息,即每次调用exec()都要是同一个RegExp 对象。
- 案例4:测试 RegExp 的 分组匹配,每个分组用 “()” 界定
var str = "2019-11-05";
// 年、月、日分别为一组
var arr = str.match(/(\d{4})[-](\d{2})[-](\d{2})/);
// 匹配到的整个字符串、每组匹配到的字符串信息都保存了下来
// ["2019-11-05", "2019", "11", "05", index: 0, input: "2019-11-05", groups: undefined]
console.log(arr);
// 可以提取每组匹配到的字符串信息,未匹配到的返回 ""
// 2019 11 05 true
console.log(RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4 == "");
// 再次调用match()后,更新RegExp对象匹配到的分组字符串信息
var arr2 = str.match(/(\d{4}[-]\d{2}[-]\d{2})/);
// 新匹配到的分组
// 2019-11-05 true
console.log(RegExp.$1, RegExp.$2 == "");
可以看到,在使用分组匹配时,会将匹配到的分组字符串信息存到 RegExp 对象,且只有再次使用分组匹配时,才能更新RegExp对象保存的分组信息。
参考链接: