函数进阶
函数的定义方式
- 函数声明
- 函数表达式
new Function
函数声明
function foo () {
}
函数表达式
var foo = function () {
}
函数声明与函数表达式的区别
- 函数声明必须有名字
- 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
- 函数表达式类似于变量赋值
- 函数表达式可以没有名字,例如匿名函数
- 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用
下面是一个根据条件定义函数的例子:
if (true) {
function f () {
console.log(1)
}
} else {
function f () {
console.log(2)
}
}
以上代码执行结果在不同浏览器中结果不一致。
不过我们可以使用函数表达式解决上面的问题:
var f
if (true) {
f = function () {
console.log(1)
}
} else {
f = function () {
console.log(2)
}
}
函数的调用方式
- 普通函数
- 构造函数
- 对象方法
函数内 this
指向的不同场景
函数的调用方式决定了 this
指向的不同:
调用方式 | 非严格模式 | 备注 |
---|---|---|
普通函数调用 | window | 严格模式下是 undefined |
构造函数调用 | 实例对象 | 原型方法中 this 也是实例对象 |
对象方法调用 | 该方法所属对象 | 紧挨着的对象 |
事件绑定方法 | 绑定事件对象 | |
定时器函数 | window |
这就是对函数内部 this 指向的基本整理,写代码写多了自然而然就熟悉了。
函数也是对象
- 所有函数都是
Function
的实例
call、apply、bind
那了解了函数 this 指向的不同场景之后,我们知道有些情况下我们为了使用某种特定环境的 this 引用,
这时候时候我们就需要采用一些特殊手段来处理了,例如我们经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。
然而实际上对于这种做法我们的 JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。
这就是接下来我们要学习的 call、apply、bind 三个函数方法。
call
call()
方法调用一个函数, 其具有一个指定的 this
值和分别地提供的参数(参数的列表)。
<p class="danger">
注意:该方法的作用和 apply()
方法类似,只有一个区别,就是 call()
方法接受的是若干个参数的列表,而 apply()
方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.call(thisArg[, arg1[, arg2[, ...]]])
参数:
-
thisArg
- 在 fun 函数运行时指定的 this 值
- 如果指定了 null 或者 undefined 则内部 this 指向 window
-
arg1, arg2, ...
- 指定的参数列表
apply
apply()
方法调用一个函数, 其具有一个指定的 this
值,以及作为一个数组(或类似数组的对象)提供的参数。
<p class="danger">
注意:该方法的作用和 call()
方法类似,只有一个区别,就是 call()
方法接受的是若干个参数的列表,而 apply()
方法接受的是一个包含多个参数的数组。
</p>
语法:
fun.apply(thisArg, [argsArray])
参数:
thisArg
argsArray
apply()
与 call()
非常相似,不同之处在于提供参数的方式。apply()
使用参数数组而不是一组参数列表。例如:
fun.apply(this, ['eat', 'bananas'])
bind
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
- thisArg
- 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
- arg1, arg2, ...
- 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
返回值:
返回由指定的this值和初始化参数改造的原函数拷贝。
示例1:
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域
// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
示例2:
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' +
this.petalCount + ' petals!');
};
var flower = new LateBloomer();
flower.bloom(); // 一秒钟后, 调用'declare'方法
小结
- call 和 apply 特性一样
- 都是用来调用函数,而且是立即调用
- 但是可以在调用函数的同时,通过第一个参数指定函数内部
this
的指向 - call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可
- apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
- 如果第一个参数指定了
null
或者undefined
则内部 this 指向 window
- bind
- 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
- 它和 call、apply 最大的区别是:bind 不会调用
- bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
- 在 bind 的同时,以参数列表的形式进行传递
- 在调用的时候,以参数列表的形式进行传递
- 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
- 两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部
函数的其它成员
- arguments
- 实参集合
- caller
- 函数的调用者
- length
- 形参的个数
- name
- 函数的名称
function fn(x, y, z) {
console.log(fn.length) // => 形参的个数
console.log(arguments) // 伪数组实参参数集合
console.log(arguments.callee === fn) // 函数本身
console.log(fn.caller) // 函数的调用者
console.log(fn.name) // => 函数的名字
}
function f() {
fn(10, 20, 30)
}
f()
高阶函数
- 函数可以作为参数
- 函数可以作为返回值
作为参数
function eat (callback) {
setTimeout(function () {
console.log('吃完了')
callback()
}, 1000)
}
eat(function () {
console.log('去唱歌')
})
作为返回值
function genFun (type) {
return function (obj) {
return Object.prototype.toString.call(obj) === type
}
}
var isArray = genFun('[object Array]')
var isObject = genFun('[object Object]')
console.log(isArray([])) // => true
console.log(isArray({})) // => true
函数闭包
function fn () {
var count = 0
return {
getCount: function () {
console.log(count)
},
setCount: function () {
count++
}
}
}
var fns = fn()
fns.getCount() // => 0
fns.setCount()
fns.getCount() // => 1
作用域、作用域链、预解析
- 全局作用域
- 函数作用域
- 没有块级作用域
{
var foo = 'bar'
}
console.log(foo)
if (true) {
var a = 123
}
console.log(a)
作用域链示例代码:
var a = 10
function fn () {
var b = 20
function fn1 () {
var c = 30
console.log(a + b + c)
}
function fn2 () {
var d = 40
console.log(c + d)
}
fn1()
fn2()
}
- 内层作用域可以访问外层作用域,反之不行
什么是闭包
闭包就是能够读取其他函数内部变量的函数,
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,
因此可以把闭包简单理解成 “定义在一个函数内部的函数”。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的用途:
- 可以在函数外部读取函数内部成员
- 让函数内成员始终存活在内存中
一些关于闭包的例子
示例1:
var arr = [10, 20, 30]
for(var i = 0; i < arr.length; i++) {
arr[i] = function () {
console.log(i)
}
}
示例2:
console.log(111)
for(var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
console.log(222)
示例3:投票
示例4:判断类型
示例5:沙箱模式
闭包的思考题
思考题 1:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
}
};
console.log(object.getNameFunc()())
思考题 2:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
}
};
console.log(object.getNameFunc()())
小结
函数递归
递归执行模型
function fn1 () {
console.log(111)
fn2()
console.log('fn1')
}
function fn2 () {
console.log(222)
fn3()
console.log('fn2')
}
function fn3 () {
console.log(333)
fn4()
console.log('fn3')
}
function fn4 () {
console.log(444)
console.log('fn4')
}
fn1()
举个栗子:计算阶乘的递归函数
function factorial (num) {
if (num <= 1) {
return 1
} else {
return num * factorial(num - 1)
}
}
递归应用场景
- 深拷贝
- 菜单树
- 遍历 DOM 树
正则表达式
- 了解正则表达式基本语法
- 能够使用JavaScript的正则对象
正则表达式简介
什么是正则表达式
正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用。正则表通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
正则表达式的作用
- 给定的字符串是否符合正则表达式的过滤逻辑(匹配)
- 可以通过正则表达式,从字符串中获取我们想要的特定部分(提取)
- 强大的字符串替换能力(替换)
正则表达式的特点
- 灵活性、逻辑性和功能性非常的强
- 可以迅速地用极简单的方式达到字符串的复杂控制
- 对于刚接触的人来说,比较晦涩难懂
正则表达式的测试
- 在线测试正则
- 工具中使用正则表达式
- sublime/vscode/word
- 演示替换所有的数字
正则表达式的组成
- 普通字符
- 特殊字符(元字符):正则表达式中有特殊意义的字符
示例演示:
-
\d
匹配数字 -
ab\d
匹配 ab1、ab2
元字符串
通过测试工具演示下面元字符的使用
常用元字符串
元字符 | 说明 |
---|---|
\d | 匹配数字 |
\D | 匹配任意非数字的字符 |
\w | 匹配字母或数字或下划线 |
\W | 匹配任意不是字母,数字,下划线 |
\s | 匹配任意的空白符 |
\S | 匹配任意不是空白符的字符 |
. | 匹配除换行符以外的任意单个字符 |
^ | 表示匹配行首的文本(以谁开始) |
$ | 表示匹配行尾的文本(以谁结束) |
限定符
限定符 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
其它
[] 字符串用中括号括起来,表示匹配其中的任一字符,相当于或的意思
[^] 匹配除中括号以内的内容
\ 转义符
| 或者,选择两者中的一个。注意|将左右两边分为两部分,而不管左右两边有多长多乱
() 从两个直接量中选择一个,分组
eg:gr(a|e)y匹配gray和grey
[\u4e00-\u9fa5] 匹配汉字
案例
验证手机号:
^\d{11}$
验证邮编:
^\d{6}$
验证日期 2012-5-01
^\d{4}-\d{1,2}-\d{1,2}$
验证邮箱 xxx@itcast.cn:
^\w+@\w+\.\w+$
验证IP地址 192.168.1.10
^\d{1,3}\(.\d{1,3}){3}$
JavaScript 中使用正则表达式
创建正则对象
方式1:
var reg = new Regex('\d', 'i');
var reg = new Regex('\d', 'gi');
方式2:
var reg = /\d/i;
var reg = /\d/gi;
参数
标志 | 说明 |
---|---|
i | 忽略大小写 |
g | 全局匹配 |
gi | 全局匹配+忽略大小写 |
正则匹配
// 匹配日期
var dateStr = '2015-10-10';
var reg = /^\d{4}-\d{1,2}-\d{1,2}$/
console.log(reg.test(dateStr));
匹配正则表达式
// console.log(/./.test("除了回车换行以为的任意字符"));//true
// console.log(/.*/.test("0个到多个"));//true
// console.log(/.+/.test("1个到多个"));//true
// console.log(/.?/.test("哈哈"));//true
// console.log(/[0-9]/.test("9527"));//true
// console.log(/[a-z]/.test("what"));//true
// console.log(/[A-Z]/.test("Are"));//true
// console.log(/[a-zA-Z]/.test("干啥子"));//false
// console.log(/[0-9a-zA-Z]/.test("9ebg"));//true
// console.log(/b|(ara)/.test("abra"));//true
// console.log(/[a-z]{2,3}/.test("arfsf"));//true
console.log(/\d/.test("998"));//true
console.log(/\d*/.test("998"));//true
console.log(/\d+/.test("998"));//true
console.log(/\d{0,}/.test("998"));//true
console.log(/\d{2,3}/.test("998"));//true
console.log(/\D/.test("eat"));//true
console.log(/\s/.test(" "));//true
console.log(/\S/.test("嘎嘎"));//true
console.log(/\w/.test("_"));//true
console.log(/\W/.test("_"));//true
正则表达式案例
1.验证密码强弱
2.验证邮箱:[0-9a-zA-Z_.-]+[@][0-9a-zA-Z._-]+([.][a-zA-Z]+){1,2}
3.验证中文名字[\u4e00-\u9fa5]
正则提取
// 1. 提取工资
var str = "张三:1000,李四:5000,王五:8000。";
var array = str.match(/\d+/g);
console.log(array);
// 2. 提取email地址
var str = "123123@xx.com,fangfang@valuedopinions.cn 286669312@qq.com 2、emailenglish@emailenglish.englishtown.com 286669312@qq.com...";
var array = str.match(/\w+@\w+\.\w+(\.\w+)?/g);
console.log(array);
// 3. 分组提取
// 3. 提取日期中的年部分 2015-5-10
var dateStr = '2016-1-5';
// 正则表达式中的()作为分组来使用,获取分组匹配到的结果用Regex.$1 $2 $3....来获取
var reg = /(\d{4})-\d{1,2}-\d{1,2}/;
if (reg.test(dateStr)) {
console.log(RegExp.$1);
}
// 4. 提取邮件中的每一部分
var reg = /(\w+)@(\w+)\.(\w+)(\.\w+)?/;
var str = "123123@xx.com";
if (reg.test(str)) {
console.log(RegExp.$1);
console.log(RegExp.$2);
console.log(RegExp.$3);
}
正则替换
// 1. 替换所有空白
var str = " 123AD asadf asadfasf adf ";
str = str.replace(/\s/g,"xx");
console.log(str);
// 2. 替换所有,|,
var str = "abc,efg,123,abc,123,a";
str = str.replace(/,|,/g, ".");
console.log(str);
案例:表单验证
QQ号:<input type="text" id="txtQQ"><span></span><br>
邮箱:<input type="text" id="txtEMail"><span></span><br>
手机:<input type="text" id="txtPhone"><span></span><br>
生日:<input type="text" id="txtBirthday"><span></span><br>
姓名:<input type="text" id="txtName"><span></span><br>
//获取文本框
var txtQQ = document.getElementById("txtQQ");
var txtEMail = document.getElementById("txtEMail");
var txtPhone = document.getElementById("txtPhone");
var txtBirthday = document.getElementById("txtBirthday");
var txtName = document.getElementById("txtName");
//
txtQQ.onblur = function () {
//获取当前文本框对应的span
var span = this.nextElementSibling;
var reg = /^\d{5,12}$/;
//判断验证是否成功
if(!reg.test(this.value) ){
//验证不成功
span.innerText = "请输入正确的QQ号";
span.style.color = "red";
}else{
//验证成功
span.innerText = "";
span.style.color = "";
}
};
//txtEMail
txtEMail.onblur = function () {
//获取当前文本框对应的span
var span = this.nextElementSibling;
var reg = /^\w+@\w+\.\w+(\.\w+)?$/;
//判断验证是否成功
if(!reg.test(this.value) ){
//验证不成功
span.innerText = "请输入正确的EMail地址";
span.style.color = "red";
}else{
//验证成功
span.innerText = "";
span.style.color = "";
}
};
表单验证部分,封装成函数:
var regBirthday = /^\d{4}-\d{1,2}-\d{1,2}$/;
addCheck(txtBirthday, regBirthday, "请输入正确的出生日期");
//给文本框添加验证
function addCheck(element, reg, tip) {
element.onblur = function () {
//获取当前文本框对应的span
var span = this.nextElementSibling;
//判断验证是否成功
if(!reg.test(this.value) ){
//验证不成功
span.innerText = tip;
span.style.color = "red";
}else{
//验证成功
span.innerText = "";
span.style.color = "";
}
};
}
通过给元素增加自定义验证属性对表单进行验证:
<form id="frm">
QQ号:<input type="text" name="txtQQ" data-rule="qq"><span></span><br>
邮箱:<input type="text" name="txtEMail" data-rule="email"><span></span><br>
手机:<input type="text" name="txtPhone" data-rule="phone"><span></span><br>
生日:<input type="text" name="txtBirthday" data-rule="date"><span></span><br>
姓名:<input type="text" name="txtName" data-rule="cn"><span></span><br>
</form>
// 所有的验证规则
var rules = [
{
name: 'qq',
reg: /^\d{5,12}$/,
tip: "请输入正确的QQ"
},
{
name: 'email',
reg: /^\w+@\w+\.\w+(\.\w+)?$/,
tip: "请输入正确的邮箱地址"
},
{
name: 'phone',
reg: /^\d{11}$/,
tip: "请输入正确的手机号码"
},
{
name: 'date',
reg: /^\d{4}-\d{1,2}-\d{1,2}$/,
tip: "请输入正确的出生日期"
},
{
name: 'cn',
reg: /^[\u4e00-\u9fa5]{2,4}$/,
tip: "请输入正确的姓名"
}];
addCheck('frm');
//给文本框添加验证
function addCheck(formId) {
var i = 0,
len = 0,
frm =document.getElementById(formId);
len = frm.children.length;
for (; i < len; i++) {
var element = frm.children[i];
// 表单元素中有name属性的元素添加验证
if (element.name) {
element.onblur = function () {
// 使用dataset获取data-自定义属性的值
var ruleName = this.dataset.rule;
var rule =getRuleByRuleName(rules, ruleName);
var span = this.nextElementSibling;
//判断验证是否成功
if(!rule.reg.test(this.value) ){
//验证不成功
span.innerText = rule.tip;
span.style.color = "red";
}else{
//验证成功
span.innerText = "";
span.style.color = "";
}
}
}
}
}
// 根据规则的名称获取规则对象
function getRuleByRuleName(rules, ruleName) {
var i = 0,
len = rules.length;
var rule = null;
for (; i < len; i++) {
if (rules[i].name == ruleName) {
rule = rules[i];
break;
}
}
return rule;
}
补充
伪数组和数组
在JavaScript中,除了5种原始数据类型之外,其他所有的都是对象,包括函数(Function)。
对象与数组的关系
在说区别之前,需要先提到另外一个知识,就是 JavaScript 的原型继承。
所有 JavaScript 的内置构造函数都是继承自 Object.prototype
。
在这个前提下,可以理解为使用 new Array()
或 []
创建出来的数组对象,都会拥有 Object.prototype
的属性值。
var obj = {};// 拥有 Object.prototype 的属性值
var arr = [];
//使用数组直接量创建的数组,由于 Array.prototype 的属性继承自 Object.prototype,
//那么,它将同时拥有 Array.prototype 和 Object.prototype 的属性值
可以得到对象和数组的第一个区别:对象没有数组 Array.prototype 的属性值。
什么是数组
数组具有一个最基本特征:索引,这是对象所没有的,下面来看一段代码:
var obj = {};
var arr = [];
obj[2] = 'a';
arr[2] = 'a';
console.log(obj[2]); // => a
console.log(arr[2]); // => a
console.log(obj.length); // => undefined
console.log(arr.length); // => 3
- obj[2]输出'a',是因为对象就是普通的键值对存取数据
- 而arr[2]输出'a' 则不同,数组是通过索引来存取数据,arr[2]之所以输出'a',是因为数组arr索引2的位置已经存储了数据
- obj.length并不具有数组的特性,并且obj没有保存属性length,那么自然就会输出undefined
- 而对于数组来说,length是数组的一个内置属性,数组会根据索引长度来更改length的值
- 为什么arr.length输出3,而不是1
- 在给数组添加元素时,并没有按照连续的索引添加,所以导致数组的索引不连续,那么就导致索引长度大于元素个数
什么是伪数组
- 拥有 length 属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理,这里你可以当做是个非负整数串来理解)
- 不具有数组所具有的方法
伪数组,就是像数组一样有 length
属性,也有 0、1、2、3
等属性的对象,看起来就像数组一样,但不是数组,比如:
var fakeArray = {
"0": "first",
"1": "second",
"2": "third",
length: 3
};
for (var i = 0; i < fakeArray.length; i++) {
console.log(fakeArray[i]);
}
Array.prototype.join.call(fakeArray,'+');
常见的伪数组有:
- 函数内部的
arguments
- DOM 对象列表(比如通过
document.getElementsByTags
得到的列表) - jQuery 对象(比如
$("div")
)
伪数组是一个 Object,而真实的数组是一个 Array。
伪数组存在的意义,是可以让普通的对象也能正常使用数组的很多方法,比如:
var arr = Array.prototype.slice.call(arguments);
Array.prototype.forEach.call(arguments, function(v) {
// 循环arguments对象
});
// push
// some
// every
// filter
// map
// ...
以上在借用数组的原型方法的时候都可以通过数组直接量来简化使用:
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
;[].push.call(obj, 'd')
console.log([].slice.call(obj))
;[].forEach.call(obj, function (num, index) {
console.log(num)
})
小结
- 对象没有数组 Array.prototype 的属性值,类型是 Object ,而数组类型是 Array
- 数组是基于索引的实现, length 会自动更新,而对象是键值对
- 使用对象可以创建伪数组,伪数组可以正常使用数组的大部分方法