一、正则表达式
1. 正则表达式的组成
- 普通字符abc 123
- 特殊字符(元字符):正则表达式中有特殊意义的字符\d \w
1.1 字符类
字符类 | 说明 |
---|---|
[abc] | 只能是a b c中的任意一个,也就是说[]里面的内容只能选择任意一个 |
[^abc] | 任何字符,除了 a、b 或 c(否定) |
[a-z] | a 到 z 中的任意一个 |
[^a-z] | 非a 到 z 中的任意一个 |
[a-zA-Z] | a 到 z 或 A 到 Z,两头的字母包括在内(范围) |
[0-9] | 0-9之间的任意一个 |
[\u4e00-\u9fa5] | 表示汉字的取值范围 |
1.2 元字符
元字符 | 说明 |
---|---|
\d | 匹配数字 |
\D | 匹配任意非数字的字符 |
\w | 匹配字母或数字或下划线 |
\W | 匹配任意不是字母,数字,下划线 |
\s | 匹配任意的空白符 |
\S | 匹配任意不是空白符的字符 |
. | 匹配除换行符以外的任意单个字符 |
^ | 表示匹配行首的文本(以谁开始) |
$ | 表示匹配行尾的文本(以谁结束) |
1.3 限定符
限定符 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复>=n次 |
{n,m} | 重复n到m次 |
1.4 条件
条件 | 说明 |
---|---|
(?![0-9]+$) | 不能全是数字 |
(?![a-zA-Z]+$) | 不能全是字母 |
1.5 其他
其他 | 说明 |
---|---|
[] | 字符串用中括号括起来,表示匹配其中的任一字符 |
[^] | 匹配除中括号以内的内容 |
\ | 转义符 |
| | 或者,选择两者中的一个。注意 |将左右两边分为两部分,而不管左右两边有多长多乱 |
() | 从两个直接量中选择一个,分组 |
1.6 常用的简单案例
验证手机号:
^\d{11}$
验证邮编:
^\d{6}$
验证日期 2012-5-01
^\d{4}-\d{1,2}-\d{1,2}$
验证邮箱 xxx@newcapec.com.cn:
^\w+@\w+\.\w+$
验证IP地址 192.168.1.10
^\d{1,3}(.\d{1,3}){3}$
2. 使用正则表达式
标志 | 说明 |
---|---|
i | 忽略大小写 |
g | 全局匹配 |
gi | 全局匹配+忽略大小写 |
方式1:
var reg = new RegExp('\d', 'i');
var reg = new RegExp('\d', 'gi');
方式2:
var reg = /\d/i;
var reg = /\d/gi;
3. 正则匹配
/*
正则的作用: 匹配、替换、提取
1、作为正则对象来用,用来调用匹配方法 (匹配)
正则对象.test(字符串)
var reg = /^正则规则$/
new RegExp(pattern, attributes);
pattern:一个字符串,指定了正则表达式的模式或其他正则表达式。
attributes:一个可选的字符串,包含属性“g”、“i”和“m”,分别用于指定全局
匹配、区分大小写的匹配和多行匹配
regexp.test(string)
string:要检测的字符串。
如果字符串string中含有与regexp匹配的文本,就返回true,否则返回false。
2、作为正则参数来用,用来作为方法的参数 (替换、提取)
字符串.replace(正则,字符串)
字符串.replace(/正则规则/,字符串)
*/
// 电话号码
var tel_reg = /^\d{11}$/;
var tel = "12311111111";
console.log("电话号码是否符合规则:" + tel_reg.test(tel));
// 邮编
var postcode_reg = /^\d{6}$/;
// 验证日期 2012-5-01
var date_reg = /^\d{4}(-\d{1,2}){1,2}$/;
// var date_reg = /^\d{4}-\d{1,2}-\d{1,2}$/;
var date1 = "2020-05";
var date2 = "2020-09-1";
var date3 = "2020";
console.log("日期是否符合规则:" + date_reg.test(date1));
console.log("日期是否符合规则:" + date_reg.test(date2));
console.log("日期是否符合规则:" + date_reg.test(date3));
// 邮箱 .com.cn
var email_reg = /^[A-Za-z0-9]{5,}@[a-z0-9]{2,5}(\.[a-z]{2,3}){1,2}$/;
// var email_reg = /^\w+@\w+\.\w+$/;
var email1 = "2@q.com";
var email2 = "zhangsan@163.com";
var email3 = "32132@qq.com";
var email4 = "zhangsan123@yahoo.com.cn";
console.log("邮箱是否符合规则:" + email_reg.test(email1));
console.log("邮箱是否符合规则:" + email_reg.test(email2));
console.log("邮箱是否符合规则:" + email_reg.test(email3));
console.log("邮箱是否符合规则:" + email_reg.test(email4));
// ip校验
var ip_reg = /^\d{1,3}(.\d{1,3}){3}$/;
// 用户名,8-20为字母、数子、下划线,不能全是数字
var userName_reg = /^(?![0-9]+$)\w{8,20}$/;
var userName = "12345678";
console.log("用户名是否符合规则:" + userName_reg.test(userName))
4. 正则提取
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 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);
}
</script>
</body>
</html>
5. 正则替换
// 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);
6. 表单验证
6.1 不封装
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>
<script>
//获取文本框
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 = "";
}
};
</script>
6.2 封装
<script>
//获取文本框
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");
var qq_flag = false;
var email_flag = false;
// 封装共有函数
function check(element, reg, tips) {
element.onfocus = function () {
var nextSpan = this.nextElementSibling;
nextSpan.innerHTML = "请输入您的" + tips;
nextSpan.className = "";
}
element.onblur = function () {
var val = this.value;
var nextSpan = this.nextElementSibling;
if (val.replace(/\s/g, "") === "") {
nextSpan.innerHTML = tips + "不能为空";
nextSpan.className = "error";
} else {
var reg_obj = reg;
if (reg_obj.test(val)) {
nextSpan.innerHTML = "格式正确";
nextSpan.className = "correct";
element.correct = true;
} else {
nextSpan.innerHTML = tips + "格式错误";
nextSpan.className = "error";
}
}
}
}
// 验证qq
var QQ_REG = /^\d{5,}$/;
check(txtQQ, QQ_REG, "QQ号码")
// 验证email
var email_reg = /^[A-Za-z0-9]{5,}@[a-z0-9]{2,5}(\.[a-z]{2,3}){1,2}$/;
check(txtEMail, email_reg, "邮箱地址")
// 验证手机号
var tel_reg = /^\d{11}$/;
check(txtPhone, tel_reg, "手机号码")
var f1 = document.getElementById("f1");
f1.onsubmit = function () {
console.log(txtQQ.correct);
if (!(txtQQ.correct && txtEMail.correct && txtPhone.correct)) {
return false;
}
}
</script>
7. 常用正则汇总
7.1 数字的验证
数字:^[0-9]*$
n位的数字:^\d{n}$
至少n位的数字:^\d{n,}$
m-n位的数字:^\d{m,n}$
零和非零开头的数字:^(0|[1-9][0-9]*)$
非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(\.[0-9]{1,2})?$
带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})$
正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
有两位小数的正实数:^[0-9]+(\.[0-9]{2})?$
有1~3位小数的正实数:^[0-9]+(\.[0-9]{1,3})?$
非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
非负整数:^\d+$ 或 ^[1-9]\d*|0$
非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?
\.0+|0$
正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*
[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-
9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
7.2 字符的验证
汉字:^[\u4e00-\u9fa5]{0,}$
英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
长度为3-20的所有字符:^.{3,20}$
由26个英文字母组成的字符串:^[A-Za-z]+$
由26个大写英文字母组成的字符串:^[A-Z]+$
由26个小写英文字母组成的字符串:^[a-z]+$
由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5AZa-z0-9]{2,20}$
可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
禁止输入含有~的字符:[^~\x22]+
7.3 特殊需求的验证
Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?
InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXXXXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
电话号码正则表达式(支持手机号码,3-4位区号,7-8位直播号码,1-4位分机号):
((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-
(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)
身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|
(^\d{17}(\d|X|x)$)
帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间):^(?=.*\d)(?=.*
[a-z])(?=.*[A-Z])[a-zA-Z0-9]{8,10}$
强密码(必须包含大小写字母和数字的组合,可以使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[az])(?=.*[A-Z]).{8,10}$
日期格式:^\d{4}-\d{1,2}-\d{1,2}
一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
钱的输入格式:
有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和
"10,000":^[1-9][0-9]*$
这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|
[1-9][0-9]*)$
一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是
负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过
的:^[0-9]+(.[0-9]{2})?$
这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]
{3})*(.[0-9]{1,2})?$
1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,
[0-9]{3})*)(.[0-9]{1,2})?$
备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)
最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
中文字符的正则表达式:[\u4e00-\u9fa5]
双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII
字符计1))
空白行的正则表达式:\n\s*\r (可以用来删除空白行)
HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> ( 首尾空白字符的正则表达式:^\s*|\s*$或
(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?
\\d?\\d))
正则表达式在线测试:https://c.runoob.com/front-end/854
二、对象
1. 创建对象
1.1 new Object()
var person = new Object()
person.name = 'Jack'
person.age = 18
person.sayName = function () {
console.log(this.name)
}
1.2 字面量创建
var person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?代码太过冗余,重复性 太高。
1.3 工厂函数创建对象
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象 识别的问题(即怎样知道一个对象的类型)。
1.4 构造函数
1.4.1 语法
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var p1 = new Person('Jack', 18)
p1.sayName() // => Jack
var p2 = new Person('Mike', 23)
p2.sayName() // => Mike
1.4.2 分析
function Person (name, age) {
// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
// var instance = {}
// 然后让内部的 this 指向 instance 对象
// this = instance
// 接下来所有针对 this 的操作实际上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将 this 返回,也就是 instance
// return this
}
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题,每一个实例都有一个重复的sayName()方法,浪费内存
三、原型
1. 使用原型(prototype)解决内存浪费问题
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的所拥有。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type 属性和 sayName() 方法,其实都是同一个内存地址,指向 prototype 对象, 因此就提高了运行效率。
2. 构造函数、实例、原型三者之间的关系
任何函数都具有一个 prototype 属性,该属性是一个对象。
function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {
console.log('hi!')
}
构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
console.log(F.prototype.constructor === F) // => true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
实例对象可以直接访问原型对象成员。
instance.sayHi() // => hi!
总结:
- 任何函数都具有一个 prototype 属性,该属性是一个对象
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针
__proto__
- 所有实例都直接或间接继承了原型对象的成员
四、原型链
1. 原型的简介
在浏览器控制台中,试着创建一个对象字面量:
const myObject = {
city: "Madrid",
greet() {
console.log(`来自 ${this.city} 的问候`);
},
};
myObject.greet(); // 来自 Madrid 的问候
这里有一个对象,它具有数据属性 city
和方法 greet()
。如果你在控制台中输入对象的名称,然后跟随一个小数点(如同 myObject.
),控制台会列出该对象可用的一系列属性。你会看到,除了 city
和 greet
外,还有很多其他属性!
__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
city
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf
试着访问其中一个:
myObject.toString(); // "[object Object]"
它可以成功调用(即使你不知道 toString()
到底在做什么)。
这些额外的属性是什么,它们是从哪里来的?
JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null
作为其原型的对象上。
备注: 指向对象原型的属性并不是 prototype
。它的名字不是标准的,但实际上所有浏览器都使用 __proto__
。访问对象原型的标准方法是 Object.getPrototypeOf()
。
2. 属性的查找方法
当你试图访问一个对象的属性时:如果在对象本身中找不到该属性,就会在原型中搜索该属性。如果仍然找不到该属性,那么就搜索原型的原型,以此类推,直到找到该属性,或者到达链的末端,在这种情况下,返回 undefined
。
所以,在调用 myObject.toString()
时,浏览器做了这些事情:
- 在
myObject
中寻找toString
属性 myObject
中找不到toString
属性,故在myObject
的原型对象中寻找toString
- 其原型对象拥有这个属性,然后调用它。
myObject
的原型是什么?为了找到答案,我们可以使用 Object.getPrototypeOf()
函数:
Object.getPrototypeOf(myObject); // Object { }
那Object { }
的原型是什么?继续通过上述方法寻找
myObject = Object.getPrototypeOf(myObject); // Object { };
console.log(myObject)// Object{}
myObject = Object.getPrototypeOf(myObject); // Object { };
console.log(F.prototype.constructor === F) // null
有个对象叫 Object.prototype
,它是最基础的原型,所有对象默认都拥有它。Object.prototype
的原型是 null
,所以它位于原型链的终点:
一个对象的原型并不总是 Object.prototype
,试试这段代码:
const myDate = new Date();
let object = myDate;
do {
object = Object.getPrototypeOf(object);
console.log(object);
} while (object);
// Date.prototype
// Object { }
// null
这段代码创建了 Date
对象,然后遍历了它的原型链,记录并输出了原型。从中我们知道 myDate
的原型是 Date.prototype
对象,它(Date.prototype
)的原型是 Object.prototype
。
实际上,如果调用了你所熟悉的方法(如 myDate2.getMonth()
),是在 Date.prototype
上定义的方法调用的。
3. 属性遮蔽
如果你在一个对象中定义了一个属性,而在该对象的原型中定义了一个同名的属性,会发生什么?我们来看看:
const myDate = new Date(1995, 11, 17);
console.log(myDate.getYear()); // 95
myDate.getYear = function () {
console.log("别的东西!");
};
myDate.getYear(); // '别的东西!'
鉴于对原型链的描述,这应该是可以预测的。当我们调用 getYear()
时,浏览器首先在 myDate
中寻找具有该名称的属性,如果 myDate
没有定义该属性,才检查原型。因此,当我们给 myDate
添加 getYear()
时,就会调用 myDate
中的版本。
这叫做属性的“遮蔽”。
function Person(name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.evr = {
breathe: "氧气",
drink: "水"
}
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person("zhangsan", 12);
var p2 = new Person("lis", 11);
p1.type = "cat"
console.log(p1.type); // cat
console.log(p1.__proto__.type) // human
// p1.evr = "xx";
// console.log(p1.evr); // xx
// console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "水"}
p1.evr.drink = "纯净水";
console.log(p1.evr); // {breathe: "氧气", drink: "纯净水"}
console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "纯净水"}
4. 更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。 为减少不必要的输 入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
在该示例中,我们将 Person.prototype 重置到了一个新的对象。
这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢 失了 constructor 成员
所以,我们为了保持 constructor 的指向正确,建议的写法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person, // => 手动将 constructor 指向正确的构造函数
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
5. 原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
五、继承
1. 继承的实现方式
1.1 对象字面量继承
var wjl = {
name: '王健林',
money: 10000000,
cars: ['玛莎拉蒂', '特斯拉'],
houses: ['别墅', '大别墅'],
play: function () {
console.log('打高尔夫');
}
}
var wsc = {
name: '王思聪'
}
// // 复制对象的成员给另一个对象
// for (var key in wjl) {
// // 不给wsc复制同名的属性
// if (wsc[key]) {
// continue;
// }
// wsc[key] = wjl[key];
// }
// console.log(wsc);
// 对象的拷贝
// 复制对象的成员给另一个对象
function extend(parent, child) {
for (var key in parent) {
// 不给wsc复制同名的属性
if (child[key]) {
continue;
}
child[key] = parent[key];
}
}
extend(wjl, wsc);
console.log(wsc);
1.2 构造函数的属性继承:(不继承原型对象)
function Person (name, age) {
this.type = 'human'
this.name = name
this.age = age
}
function Student (name, age) {
// 借用构造函数继承属性成员
// call()是改变函数中的this,直接调用函数
Person.call(this, name, age)
}
var s1 = new Student('张三', 18)
console.log(s1.type, s1.name, s1.age) // => human 张三 18
1.3 构造函数的原型方法继承: (继承原型对象)
function Person(name, age) {
this.type = 'human'
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
console.log('hello ' + this.name)
}
function Student(name, age) {
Person.call(this, name, age)
}
// 原型对象拷贝继承原型对象成员
for (var key in Person.prototype) {
Student.prototype[key] = Person.prototype[key]
}
var s1 = new Student('张三', 18)
s1.sayName() // => hello 张三
console.log(s1.type) // => human
s1.sayName() // => hello 张三
六、函数
1. 函数内 this 指向的不同场景
函数的调用方式决定了 this 指向的不同:
调用方式 | 非严格模式 | 说明 |
---|---|---|
普通函数调用 | window | 严格模式下是 undefined |
构造函数调用 | 实例对象 | 原型方法中 this 也是实例对象 |
对象方法调用 | 该方法所属对象 | 紧挨着的对象 |
事件绑定方法 | 绑定事件对象 | |
定时器函数 | window |
示例:
<ul id="ul1">
<li>我是第1个li</li>
<li>我是第2个li</li>
<li>我是第3个li</li>
<li>我是第4个li</li>
<li>我是第5个li</li>
</ul>
<script>
// 获取li的下标
var lis = document.querySelectorAll("#ul1>li");
for (var i = 0; i < lis.length; i++) {
var li = lis[i];
li.index = i;
//在鼠标函数里面,this是点击的li
li.onclick = function () {
// 通过变量来保存this
var _this = this;
// 1s之后,输出li的下标
setTimeout(function () {
// 因为在定时器里面,this是window
console.log(_this.index);
}, 1000)
}
}
</script>
2. call、apply、bind
<script>
var dog = {
name: "小狗",
eat: function (food1,food2) {
console.log(this.name+"吃"+food1+"和"+food2);
}
}
var cat = {
name: "小猫"
}
dog.eat("骨头","包子");// 小狗吃骨头和包子
// call、apply、bind都可以改变this的指向,指向第一个参数
// call是函数中的一个方法,call会直接调用函数,可以接受多个参数
dog.eat.call(cat,"鱼","耗子");// 小猫吃鱼和耗子
// apply和call的区别是,只能接收一个参数
dog.eat.apply(cat,["鱼","耗子"]);// 小猫吃鱼和耗子
// bind是函数中的一个方法,bind会创建一个新的函数,但是不会调用函数,这个新函数的this指向第一个参数
var funcEat = dog.eat.bind(cat,"鱼","耗子");// 小猫吃鱼和耗子
funcEat()
// bind也可以加括号直接调用
dog.eat.bind(cat,"鱼","耗子")();// 小猫吃鱼和耗子
</script>
七、闭包
1. 什么是闭包
闭包就是能够读取其他函数内部变量的函数, 由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量, 因此可以把闭包简单理解成 “定义在一个函数内部的函数”。 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 理解一: 闭包是嵌套的内部函数(绝大部分人) 理解二: 包含被引用变量(函数)的对象(极少数人) 注意: 闭包存在于嵌套的内部函数中
2. 闭包的说明
闭包的用途:
- 可以在函数外部读取函数内部成员
- 让函数内成员生命周期延长
如何产生闭包:
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
产生闭包的条件:
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
3. 具体使用
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍历加监听
// for (var i = 0,length=btns.length; i < length; i++) {
// var btn = btns[i]
// //将btn所对应的下标保存在btn上
// btn.index = i
// btn.onclick = function () {
// alert('第'+(this.index+1)+'个')
// }
// }
/**/
//利用闭包
for (var i = 0, length = btns.length; i < length; i++) {
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第' + (j + 1) + '个')
}
})(i)
}
</script>
<script>
function fn1() {
var a = 2;
return function () {
a++;
console.log(a);
};
}
var fn = fn1();
fn()
fn();
fn();
fn();
fn1 = null;
fn();
</script>
</body>
4. 闭包的缺点及解决
缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
解决方案
- 能不用闭包就不用 及时释放
<script type="text/javascript">
function fn1() {
var arr = new Array[100000]
function fn2() {
console.log(arr.length)
}
return fn2
}
var f = fn1()
f()
f = null //让内部函数成为垃圾对象-->回收闭包
</script>
5. 闭包导致的内存泄漏
浏览器的垃圾回收机制:(gc)
gc会回收掉,程序无法触及的内存区域;
所以,当程序不需要某一变量的时候,如果不对该变量进行处理,则gc不会回收。
例如:
<script>
function myFunction() {
var count = 0;
return function() {
return count++;
}
}
var func = myFunction();
function listeners() {
var i = func();
if(i == 3){
// 防止内存泄漏
window.removeEventListener("click", listeners);
// 如果不写则会导致内存泄漏
// func = null;
// listeners = null;
}
console.log(i);
}
window.addEventListener("click",listeners);
</script>
如果不对func=null,listeners=null,程序还可以继续调用该函数,还可以触及,gc就不会回收,因此就出现内存泄漏。
八、防抖
在进行窗口的 resize、scroll、输出框内容校验等操作的时候,如果事件处理函数调用的频率无限制,会 加重浏览器的负担,导致用户体验非常之差。那么为了前端性能的优化也为了用户更好的体验,就可以 采用防抖(debounce)和节流(throttle)的方式来到达这种效果,减少调用的频率。
函数防抖是指当一个动作连续触发,只执行最后一次。好比我们打英雄联盟或者王者荣耀的时候,比如 你按下了回城键,那么在 8 秒钟之后,就会执行回城事件,但如果你再次按下回城键,那么回城时间又 将重新计时,需要再等 8 秒才会执行回城事件。
函数节流是指一定时间内 js 方法只跑一次。好比我们打英雄联盟或者王者荣耀的时候,释放技能都有一 段冷却时间,比如 Q 技能有 5 秒的冷却时间,那么我们在 5 秒钟的时间内只能释放一次 Q 技能。
例如,都设置时间频率为 500ms,在 2 秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防 抖,则不管调动多少次方法,在 2s 后,只会执行一次。
1. 什么是防抖
**官方解释:**当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定 的时间到来之前,又一次触发了事件,就重新开始延时。
2. 应用场景
- 有些场景事件触发的频率过高(mousemove、onkeydown、onkeyup、onscroll)。
- 回调函数执行的频率过高也会有卡顿现象,可以一段时间过后进行触发去除无用操作。
3. 防抖函数封装(debounce)
<button class="btn">123</button>
<script>
//防抖函数
function debounce(fn, delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(function() {
fn()
}, delay)
}
}
function success() {
console.log('提交成功');
}
let aaa = debounce(success, 500)
// console.log(aaa);
let btn = document.querySelector('.btn')
btn.addEventListener('click', aaa)
</script>
九丶节流
1. 节流概念
**官方解释:**当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
如果持续触发事件,每隔一段时间只执行一次函数。
2. 应用场景
- scroll 事件,每隔一秒计算一次位置信息等。
- 比如我们常见的谷歌搜索框的联想功能。
- 监听浏览器的滚动加载事件。
- 高频鼠标点击事件(也可做防抖)。
- 拖拽动画。
3. 节流函数的封装(throttle)
<div id="div">1</div>
<button id="button">点击+1</button>
<script type="text/javascript">
var div=document.getElementById("div");
var btn=document.getElementById("button");
function throttle(handler,wait){
//1 声明一个全局变量存储触发事件。
var lastTime=0;
return function(){
//2 每一次触发事件,获取当前时间
var nowTime=new Date().getTime();
//3 判断当前时间与上一次触发事件,是否超过了间隔
if(nowTime-lastTime>wait){
//4 如果超过间隔时间,则执行事件处理代码,然后存储本次触发的时间
handler();
lastTime=nowTime;
}
}
}
function buy(){
div.innerHTML=parseInt(div.innerHTML)+1;
}
btn.onclick=throttle(buy,1000);
</script>
4. 防抖与节流的区别
防抖只会在最后一次事件后执行触发函数,节流不管事件多么的频繁,都会保证在规定时间段内触 发事件函数。