一. 正则表达式
(1). 最简单的正则: 一个关键词原文
(2). 如果某一位上字符有多种备选字时: [字符集]
(3). 如果[]中部分字符是连续的: [x-x]
(4). 常用的字符集简写:
[0-9] [a-z] [A-Z]
[A-Za-z] [0-9A-Za-z]
[\u4e00-\u9fa5]
(5). 预定义字符集:
\d \w=[0-9A-Za-z_] \s .
(6). 量词:
a. {n} 必须n个,
{n,m} n个<= <=m个,
{n,} n个<= 多了不限
b. * >=0个 ,
? 0个或1个 ,
+ >=1个
(7). 两个规则二选一匹配: |
(8). 将多个子规格分为一组先匹配,再和其它规则联合匹配: ()
(9). 位置: ^开头, $结尾, \b单词边界
二. String类型提供的正则相关的函数: 3件事
1. 查找敏感词:用正则查找一句话中包含的敏感词,共4种情况:
(1). 只查找一个固定的敏感词出现的位置:
a. var i=str.indexOf("敏感词",fromi)
位置的敏感词
b. 意为: 在str中查找第一个"敏感词"出现的下标位置
c. 返回值:
1). 如果找到,返回敏感词第一个字在字符串中的下标位置
2). 如果没找到,返回-1
d. 问题: 不支持正则!只能找一种固定的敏感词!
示例: 用indexOf查找敏感词
<!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>
<!--第三阶段必须都用右键 live server运行!-->
<!--网页中如果写js,都要写在script标签中-->
<script>
//请用户输入一条消息内容
var msg=prompt("请输入消息内容");
//查找消息中是否包含敏感词我草
var i=msg.indexOf("我草");
//如果找到敏感词,返回敏感词位置
//如果没找到敏感词,返回-1
//如果包含敏感词
if(i!=-1){
//就在网页中提示红字: 包含敏感词,禁止发送
//``和${}不会的同学去第一阶段复习模板字符串
// (向) 页面(中)写 (一条HTML代码片段)
document.write(`<h1 style="color:red">包含敏感词,禁止发送</h1>`)
}else{//否则如果不包含敏感词
//就在网页中提示绿字:然哥说: 消息内容
document.write(`<h1 style="color:green">然哥说: ${msg}</h1>`)
}
</script>
</body>
</html>
(2). 用正则查找多种敏感词出现的位置:
a. var i=str.search(/正则/i)
查找
b. 意为: 在字符串str中查找第一个符合正则要求的敏感词出现的位置
c. 返回值: 同indexOf完全一样!
1). 如果找到,返回敏感词第一个字在字符串中的下标位置
2). 如果没找到,返回-1
d. 问题: 虽然在猫头鹰里有个选项可以忽略大小写,但是正则表达式天生是区分大小写的!
e. 解决: 在第二个/后加后缀 i 即可,意为ignore 忽略大小写
f. 问题: 只能查找敏感词的位置,无法获得敏感词的内容
示例: 使用search代替indexOf查找敏感词
<!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>
//请用户输入一条消息内容
var msg=prompt("请输入消息内容");
//查找消息中是否包含符合正则要求的敏感词
var i=msg.search(/([我卧]|wo)\s*([艹槽操草]|cao)/i);
//如果找到敏感词,返回敏感词位置
//如果没找到敏感词,返回-1
//如果包含敏感词
if(i!=-1){
//就在网页中提示红字: 包含敏感词,禁止发送
//``和${}不会的同学去第一阶段复习模板字符串
// (向) 页面(中)写 (一条HTML代码片段)
document.write(`<h1 style="color:red">包含敏感词,禁止发送</h1>`)
}else{//否则如果不包含敏感词
//就在网页中提示绿字:然哥说: 消息内容
document.write(`<h1 style="color:green">然哥说: ${msg}</h1>`)
}
</script>
</body>
</html>
(3). 查找敏感词的内容: 2种:
a. 只查找第一个敏感词的内容和位置:
1). var arr=str.match(/正则/i)
匹配
2). 意为: 在字符串str中查找第一个符合正则要求的敏感词的内容和位置
3). 返回值: 2种:
i. 如果找到,返回一个数组,2个房间(元素):
arr: [ 0:"敏感词内容", "index":敏感词下标位置 ]
如果想获得本次找到的关键词的内容: 标准: arr["0"] 简写:arr[0]
如果想获得本次找到的关键词的下标位置: 标准: arr["index"] 简写arr.index
说明: 0和index是不能改名,因为数组是match函数返回给我们的,不是我们自己创建的!
ii. 如果没找到,返回null
4). 问题: 如果一句话中包含多个敏感词,match默认只能找第一个敏感词,无法获得所有敏感词
示例: 使用match查找敏感词的内容和位置:
<!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>
//请用户输入一条消息内容
var msg=prompt("请输入消息内容");
//查找消息中第一个敏感词的内容和位置
var arr=msg.match(/([我卧]|wo)\s*([艹槽操草]|cao)/i);
//如果找到敏感词,返回数组
//如果没找到敏感词,返回null
//如果包含敏感词
if(arr!=null){
//就在网页中提示红字: 在位置?发现敏感词?,禁止发送
//``和${}不会的同学去第一阶段复习模板字符串
// (向) 页面(中)写 (一条HTML代码片段)
document.write(`<h1 style="color:red">在位置 ${arr.index} 发现敏感词"${arr[0]}",禁止发送</h1>`)
}else{//否则如果不包含敏感词
//就在网页中提示绿字:然哥说: 消息内容
document.write(`<h1 style="color:green">然哥说: ${msg}</h1>`)
}
</script>
</body>
</html>
b. 查找所有敏感词的内容:
1). var arr=str.match(/正则/ig)
2). g: global,查找全部敏感词
3). 返回值:
i. 如果找到敏感词,则返回一个数组,只包含所有敏感词的内容,不再包含每个敏感词的位置。
ii. 如果找不到敏感词,返回null
4). 问题: 不加g时,只能找一个,无法找所有;加g时,能找所有,但是无法获得位置。
示例: 使用match查找所有敏感词的内容:
<!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>
var str="老师:请用小红 我的 朋友造句。小亮:小红是我的朋友。小然:朋友!小红是我的!";
//希望查找出str中所有以小字开头的人名
//因为汉字没有大小写之分,所以不用加i。
var arr=str.match(/小[\u4e00-\u9fa5]/g);
console.log(arr);
</script>
</body>
</html>
(4). 既查找每个关键词的内容,又查找每个关键词的位置:
String类型没有提供这种函数
RegExp类型提供了一个reg.exec()函数
总结: 查找方法的返回值规律
- 如果原函数返回的是下标位置,如果找不到,都返回-1
- 如果原函数返回的是一个数组或一个对象,如果找不到,都返回null
强调: 如果一个函数有可能返回null!一定先判断是不是null,再使用。并且为null的情况提供备选方案! - (未完…待续…)
2. 替换敏感词:将找到的敏感词替换为新内容,2种:
(1). 简单替换: 将所有敏感词都替换为统一的新值
a. var 变量=str.replace(/正则/ig, "新值")
替换
b. 意为: 替换字符串str中所有符合正则要求的敏感词为统一的新值!
c. 强调: 因为所有字符串类型都是不可变类型,所以,replace无权修改原字符串,而是返回替换后的新字符串。原字符串保持不变。所以,必须用变量接住返回值,才能获得替换后的新字符串。
示例: 使用replace替换所有人名为**
<!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>
var str="老师:请用小红 我的 朋友造句。小亮:小红是我的朋友。小然:朋友!小红是我的!";
// var str="you can you up!no can no bibi!";
//先用match在替换之前找一次
var arr=str.match(/小[\u4e00-\u9fa5]/g);
//然后查找结果的元素个数,也就是将来要替换的个数!
console.log(arr);//null
//希望替换str中所有以小字开头的人名为**
str=str.replace(/小[\u4e00-\u9fa5]/g, "**");
console.log(str);
//还想显示共替换了多少处!
//基础一般:
//如果match找到了敏感词,才输出arr.length
if(arr!=null){
console.log(`共替换了${arr.length}处`);
}else{//否则如果match没找到敏感词,就直接输出0处代替
console.log(`共替换了0处`);
}
//学有余力: //复习第一阶段三目运算
//console.log(`共替换了${arr!=null?arr.length:0}处`)
</script>
</body>
</html>
运行结果:
["小红", "小亮", "小红", "小然", "小红"]
老师:请用** 我的 朋友造句。**:**是我的朋友。**:朋友!**是我的!
共替换了5处
(2). 高级替换: 根据每次找到的敏感词不同,动态替换成不同的新值
a. var 变量=str.replace(/正则/ig, function(形参){
return 根据本次"形参"获得的敏感词内容,动态生成一个新值
})
b. 原理: 回调函数的学习,可以靠打桩来判断执行次数,传入参数和返回值。
1). replace会用正则表达式查找str中所有符合正则要求的敏感词
2). 每找到一个敏感词就自动调用一次回调函数。——信任
3). 每次调用回调函数时,都自动将本次找到的一个敏感词内容传给形参变量。——信任
4). 回调函数中根据本次找到的敏感词内容,动态生成一个新值,返回出来
5). replace函数获得回调函数返回的新值,并自动替换到字符串中本次找到的敏感词位置——信任
示例: 鄙视题: 将字符串中每个单词首字母改为大写:
<!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>
//鄙视题: 将字符串中每个单词首字母转为大写
var str="you can you up";
// 敏感词:y c y u
// ↓
str=str.replace(/\b[a-z]/ig, function(形参){
//打桩可以让回调函数执行过程现原形!
console.log(`调用了一次回调函数,形参=${形参},返回${形参.toUpperCase()}`);
//形参: 4种可能: y c y u
return 形参.toUpperCase();
//返回: 也4种可能Y C Y U
})
console.log(str);//"You Can You Up";
</script>
</body>
</html>
运行结果:
调用了一次回调函数,形参=y,返回Y
调用了一次回调函数,形参=c,返回C
调用了一次回调函数,形参=y,返回Y
调用了一次回调函数,形参=u,返回U
You Can You Up
(3). 衍生操作: 删除敏感词: 其实就是将敏感词替换为空字符串
变量=str.replace(/正则/ig, "")
例: 鄙视题,定义三个函数,分别去掉字符串开头的空字符,去掉结尾的空字符,以及同时去掉字符串开头和结尾的空字符:
<!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>
//鄙视题三个函数:
//修剪
//trimLeft() 可去掉字符串开头的空字符
// 可以改名
// function trimLeft(字符串){
// var str2=字符串.replace(/^\s+/,"");
// return str2;
// }
//trimRight() 可去掉字符串结尾的空字符
// function trimRight(字符串){
// var str2=字符串.replace(/\s+$/,"");
// return str2;
// }
//trim() 可去掉字符串开头和结尾的空字符
// function trim(字符串){
// //因为/^\s+|\s+$/在字符串中匹配到两组敏感词,一组是开头的空字符,一组是结尾的空字符。如果想把两组空字符都替换,必须加g。
// return 字符串.replace(/^\s+|\s+$/g,"")
// }
//测试:
var str=" hello world ";
// console.log(trimLeft(str));//"hello world "
// console.log(trimRight(str));//" hello world"
// console.log(trim(str));//"hello world"
//其实ES5和ES6标准中,已经新增了现成的trim系列函数,不用自己定义。
console.log(str.trimLeft());
console.log(str.trimRight());
console.log(str.trim());
</script>
</body>
</html>
运行结果:
hello world
hello world
hello world
3. 切割字符串: 按指定的切割符,把一个完整的字符串,切割为多段子字符串
(1). 简单切割: 切割符是固定的
a. var arr=str.split("切割符")
切割
b. 按照字符串str中指定的切割符,将字符串切割为多段子字符串
c. 返回值: 包含切割后的多段子字符串的数组
d. 强调: 切割后的结果中,不包含切割符的!
(2). 复杂切割: 切割符不是固定的,但是却能找到规律!
a. var arr=str.split(/正则/);
b. 意为: 按str中所有符合正则要求的切割符将字符串切割为多段子字符串
c. 强调: split中不用加g,也能自动找到所有符合条件的切割符!
(3). 示例: 分别使用固定切割符和正则表达式切割字符串:
<!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>
//想把电子邮件切割为用户名和域名两部分
var email="zhangdong@tedu.cn";
// |
// ["zhangdong","tedu.cn"]
// 0 1
//按@将字符串email一分为二
var arr=email.split("@");
console.log(arr);
var uname=arr[0];
var domain=arr[1];
console.log(uname, domain);
//想把一个英文的句子切割为单词数组
var str="you can you up";
//按空格切割str
var arr=str.split(" ");
console.log(arr);
//如果每个单词之间的空格个数不确定有几个?
var str="you can you up";
//按一个或多个空格切割str
var arr=str.split(/\s+/);
console.log(arr);
</script>
</body>
</html>
运行结果:
(2) ["zhangdong", "tedu.cn"]
zhangdong tedu.cn
(4) ["you", "can", "you", "up"]
(4) ["you", "can", "you", "up"]
(4). 固定套路:
a. 问题: 虽然数组和字符串都有下标,但是因为字符串和数组不是同一类型,所以字符串用不了数组家的函数!
b. 解决: 先将字符串打散为一个一个字符组成的字符数组!然后就可以调用数组家的函数。只不过,加工完成后,一定再拼接回字符串!
c. 如何将字符串打散为字符数组: 用空字符串""切割
var arr=str.split("");
d. 示例: 翻转字符串
<!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>
//数组家现成的reverse函数
var arr=[1,2,3,4,5];
arr.reverse();
//翻转
console.log(arr);
//鄙视题: 翻转字符串:
var str="helloworld";
// 0123456789.length=10
//console.log(str[1]);//e
//console.log(str.length);//10
//数组家不是有reverse()函数吗?
//错误:
//str.reverse();//报错:str.reverse is not a function
//正确:
var arr=str.split("");//1. 打散为字符数组
console.log(arr);
arr.reverse();//2. 翻转字符数组的内容
console.log(arr);
//错误: join()什么都不写,默认用,连接每个字符
//str=arr.join();
//正确:
str=arr.join("");//3. 将翻转后的字符数组无缝拼接回字符串
console.log(str);//"dlrowolleh"
</script>
</body>
</html>
运行结果:
(5) [5, 4, 3, 2, 1]
(10) ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"]
(10) ["d", "l", "r", "o", "w", "o", "l", "l", "e", "h"]
dlrowolleh
4.知识点补充
1.字符串其实是一种不可变类型!
- 不可变类型: 一旦创建值不可改变,只能整体替换!
直接后果: 所有字符串家的函数都无权直接修改原字符串。只能创建新字符串保存加工后的结果,并返回新字符串。原字符串始终保持不变!
强调: 所有字符串家的函数执行后的结果,必须用变量接住,才能保存下来! - 可变类型: 创建后,还可改变其内容。比如: 数组
比如: 数组排序arr.sort(), 向数组中追加一个新元素arr.push(xxx), 都直接修改原数组,无需用变量=接住
2. 数组本质:
1. 索引数组: 下标都是数字的数组,比如: var arr=[1,2,3];
结果: [
0: 1,
1: 2,
2: 3
]
2. 关联数组: 下标都是自定义名称,比如:
var ym=[];
ym["数学"]=89,
ym["语文"]=61,
ym["英语"]=91
结果: [
"数学": 89,
"语文": 61,
"英语": 91
]
3. 其实: js中所有数组以及对象底层都是关联数组!本质上是没有索引数组!
(1). 数组和对象底层,其实都是名值对儿的集合
(2). 每个数组的下标和每对象属性的属性名,都是字符串类型
(3). 无论访问数组的元素,还是访问对象的属性,标准写法都是["下标名称"]
(4). 简写:
a. 如果下标名称不是数字,可简写为".下标名称"
b. 如果下标是数字,只能简写为"[下标]",不能用.
因为".数字"和程序中的小数点冲突了!
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>
var arr=[1,2,3];
var ym=[];
ym["数学"]=89;
ym["语文"]=61;
ym["英语"]=91;
var lilei={
姓名:"Li Lei",
年龄:11
};
console.log(arr);
console.log(ym);
console.log(lilei);
//访问每个数组和对象中的一个成员,用标准写法
console.log(arr["0"]);
console.log(ym["数学"]);
console.log(lilei["姓名"]);
//用简写
console.log(arr[0]);
console.log(ym.数学);
console.log(lilei.姓名);
</script>
</body>
</html>
三. RegExp对象: (Regular Expression 规则表达式)
1. 什么是正则表达式对象:
内存中专门保存一条正则表达式,并提供用正则表达式执行查找和验证的函数 的对象
2. 为什么要有正则表达式对象:
因为正则表达式的语法不是js语法。js不认识正则表达式。所以,需要一种专门的对象让js认出正则表达式来。
3. 何时需要正则表达式对象:
今后只要在js中使用正则表达式都需要先创建正则表达式对象
4. 如何创建正则表达式对象: 2种
(1). 标准,用new创建:
a. var reg=new RegExp("正则","ig")
b. 强调: 一定要用"",而不是//
(2). 简写,用//创建
a. var reg=/正则/ig
(3). 问题: 正则表达式不是固定不变的,可能需要根据其他内容动态生成!
(4). 错误解决: 在//中直接写生成正则表达式的js程序语句
原因: //中是正则表达式的地盘,不认识js程序,所以写了也没用。
(5). 正确解决: 今后
a. 如果正则表达式是固定的,首选//
b. 如果正则表达式不是固定的,需要动态生成, 首选new RegExp(js表达式, "ig")
(6). 示例: 根据服务器端返回的敏感词数组,防守敏感词
<!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>
//假设从服务器端用ajax请求过来一个敏感词数组
//要求: 将来数组里有什么词,我们的程序就要防住什么词!
var arr=["青天","紫烟"];
//问题: 我们不可能提前预知从服务器返回的数组中包含哪些词!
//解决: 将arr数组中每个关键词用|相连拼接成一个字符串
console.log(arr.join("|"));
// 青天|紫烟
//我们的前端js程序
//请用户输入一条消息内容
var msg=prompt("请输入消息内容");
//先定义正则表达式对象
//问题: 自然也就不可能提前在前端程序里写死正则!
//错误的解决: 将动态生成正则表达式的js语句,直接放入//中
//原因: //中是正则表达式的地盘!//中不认识js语句!
//var reg=/arr.join("|")/;
//正确的解决: 被迫使用new RegExp("正则","ig")
//原因: new RegExp()的参数刚好需要一个字符串!而我们有无数种方法可以拼出我们想要的任意字符串!
var reg=new RegExp(arr.join("|"));
// ||
// new RegExp("青天|紫烟");
// ||
//var reg=/青天|紫烟/;
console.log(reg);
//用正则表达式对象查找消息内容中是否包含敏感词
var i=msg.search(reg);
//如果包含敏感词
if(i!=-1){
//就提示红字: 包含敏感词禁止发送
document.write(`<h1 style="color:red">包含敏感词,禁止发送</h1>`)
}else{//否则
//就显示: 然哥说: 消息内容
document.write(`<h1 style="color:green">然哥说:${msg}</h1>`)
}
</script>
</body>
</html>
5. 验证字符串格式:
(1). 问题: match()不加g,即可找内容,又可找位置,但是不能找所有;match()加g,虽然可以找所有,但是只能获得内容,无法获得位置——鱼和熊掌不可兼得!
(2). 解决: var arr=reg.exec(str)
意为查找str中第一个敏感词的内容和位置,保存在一个数组中返回
(3). 只使用一次: 和match不加g的情况是一样的!
a. 也只找第一个敏感词
b. 也返回一个数组: [ 0:"敏感词内容", index:敏感词位置 ]
(4). 如果希望用reg.exec()找所有敏感词的内容和位置:
a. 正则必须加g!
b. 反复调用reg.exec(str),直到reg.exec()返回null为止——用循环
reg.exec()聪明: 自动知道要找下一个位置的敏感词!
do{
var arr=reg.exec(str);
if(arr!=null){
获得本次找到的敏感词的内容(arr[0])和位置(arr.index)
}
}while(arr!=null);
(5). 示例: 使用reg.exec()查找一句话中每个敏感词的内容和位置:
<!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>
var str="老师:请用小红 我的 朋友造句。小亮:小红是我的朋友。小然:朋友!小红是我的!";
//只找第一个敏感词: 不要加g
// var reg=/小[\u4e00-\u9fa5]/;
// //先用match找:
// var arr=str.match(reg);
// console.log(arr);
// //再用reg.exec找:
// var arr=reg.exec(str);
// console.log(arr);
//想找所有: 加g
var reg=/小[\u4e00-\u9fa5]/g;
//先用match找:
var arr=str.match(reg);
console.log(arr);
//再reg.exec()
//反复执行,只要本次找到了敏感词(arr!=null),就有必要继续查找下一个
//do while: 因为即使不确定字符串中有没有敏感词,也必须至少查找一次时,首选do while。
do{
var arr=reg.exec(str);//返回值=match()不加g时的返回值
//arr:[ 0:"敏感词内容", index:敏感词位置]
if(arr!=null){
//console.log(arr);
console.log(`在位置${arr.index},发现敏感词 ${arr[0]}`)
}
}while(arr!=null);
</script>
</body>
</html>
运行结果:
(5) ["小红", "小亮", "小红", "小然", "小红"]
在位置5,发现敏感词 小红
在位置16,发现敏感词 小亮
在位置19,发现敏感词 小红
在位置27,发现敏感词 小然
在位置33,发现敏感词 小红
四. 函数Function
一. 回顾函数:
1. 什么是函数:
程序中专门保存一段可重用的代码片段的程序结构,再起一个名字。
2. 为什么: 重用代码
3. 何时:
只要一段代码可能被反复使用,都要先将这段代码保存在函数中。然后再反复使用函数
4. 创建函数:
function 函数名(形参变量列表){
函数体;
return 返回值;
}
5. 调用函数: var 变量=函数名(实参值列表)
6. 形参变量:
(1). 什么是: 专门接收函数执行时必需的数据的变量(虽然没有var)
(2). 为什么: 有些函数执行时,必须传入必要的数据,才能正常执行!
(3). 何时: 今后,只要一个函数执行时,必须外界传入某些数据才能正常执行时,都要定义形参变量
7. 返回值:
(1). 什么是: 一个函数的执行结果,被返回到函数外部
(2). 何时: 今后,只要函数的调用者需要获得函数的执行结果时,都要定义函数的返回值。
8. 示例: 定义一个会做饭的函数,可以接收各种食材,做饭
<!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>
function cook(菜,蛋,面){
console.log(`炒${蛋}`)
console.log(`炒${菜}`)
console.log(`${菜}和${蛋}一起炒`)
console.log(`煮${面}`)
return `香喷喷的${菜}${蛋}${面}`
}
//周一
var 碗=cook("西红柿","鸡蛋","面");
console.log(碗);
//周二: 西红柿没了,只有黄瓜;鸡蛋没了,只有辣条;面没了,只有粉丝
var 碗=cook("黄瓜","辣条","粉丝");
console.log(碗);
</script>
</body>
</html>
运行结果:
炒鸡蛋
炒西红柿
西红柿和鸡蛋一起炒
煮面
香喷喷的西红柿鸡蛋面
炒辣条
炒黄瓜
黄瓜和辣条一起炒
煮粉丝
香喷喷的黄瓜辣条粉丝
二. 创建函数: 3种方法:
1. 用声明方式创建:
(1). function 函数名(形参变量列表){
函数体
return 返回值
}
(2). 问题: 会被声明提前:
a. 什么是声明提前:
1). 在程序开始执行前
2). js引擎会提前扫描程序中的两种东西:
a var声明的变量
b function声明的函数 function 函数名(...){...}
3). 提前到当前作用域的顶部集中创建
4). 赋值留在原地!
b. 缺点: 打乱程序正常的执行顺序!——JS语言广受诟病的缺陷!
2. 用赋值方式创建:
(1). var 函数名变量=function(形参变量列表){ 函数体; return 返回值 }
(2). 优点: 函数不会被声明提前,保证了程序的正常执行顺序——很多大师级人物都用赋值方式创建函数。
(3). 揭示:
a. js中函数其实也是一个普通的对象,只不过对象中保存的是代码段不是数据而已。
b. 函数名其实就是一个很普通的变量
c. 函数名变量通过保存函数对象的唯一地址值,"引用"着函数对象
d. 当调用函数时,程序先找到函数名变量->变量中的地址->保存在地址位置的函数对象->执行函数对象中保存代码段!
c. 函数在内存中到底如何存储的
示例: 鄙视题: 判断声明提前后,程序的输出结果:
<!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>
//鄙视题:
// function fun(){ console.log(1) };
// fun(); //2
// function fun(){ console.log(2) };
// fun(); //2
var fun=function(){ console.log(1) };
fun(); //1
var fun=function(){ console.log(2) };
fun(); //2
</script>
</body>
</html>
运行结果:
1
2
3. 用new 创建:
(1). var 函数名=new Function("形参1","形参2",...,"函数体; return 返回值")
(2). 很少用!
(3). 但是揭示了: function 函数名(){ ... } //被声明提前
↓
相当于var 函数名=function(){ ... }
↓
相当于var 函数名=new Function(...)
(4). 结论: function 底层等效于 new Function()
function的本质是创建一个函数对象
三. 重载: overload
1. 问题:
一件事,根据传入实参值的不同,动态执行不同的逻辑。
2. 不好的做法:
为同一件事,每种不同的逻辑,单独定义一个函数,起一个专门的函数名
原因: 增加了函数名和单词的数量,不好记忆,也不便于使用!
3. 好的解决方法——重载:
相同函数名,不同参数列表的多个函数,在调用时,可根据传入的实参值不同,自动选择匹配的一个函数执行!
4. 何时:
今后只要根据传入的参数值不同,自动执行不同的逻辑时,都要用重载
5. 优点:
减少了函数名和单词的个数,便于记忆和使用!
6. 如何:
(1). 问题: js语言不支持传统的重载方式——多个同名函数
因为: js中多个同名函数,只有最后一个函数会覆盖之前所有同名函数而存活下来
(2). 解决: 借助于arguments对象来变向实现重载效果
a. 什么是arguments:
1). 每个函数中自带的——不用自己创建,就可直接使用
2). 在调用函数时,能自动接收所有传入函数的实参值的——内容
3). 类数组对象——存储结果
i. 什么是类数组对象: 长得像数组的对象
ii. vs 数组:
相同点: 1. 下标, 2. .length, 3. for循环遍历
不同点: 类型不同!类数组对象不是数组家孩子!而是Object家的孩子。所以,类数组对象无权使用数组家的函数!——不是一家人不进一家门!
b. 如何用arguments实现重载:
1). 只定义一个函数,只起一个统一的名字,不包含任何形参变量,函数内包含所有可能的逻辑
2). 根据arguments在调用时自动接住的实参值不同,可以在函数内动态判断决定本次该执行哪种逻辑.
7. 问题:
既然js中一个函数不定义形参,照样可以传实参值,那么今后是不是都不用定义形参了?
答: 不是! 自定义形参变量和arguments[i]
自定义形参变量: 简单, 见名知义,可以告诉调用者如何正确使用函数正确传参
——今后依然首选自定义形参变量
只有参数个数不确定时,才被迫使用arguments代替!
8. 示例:
使用重载实现一个pay函数,支持三种支付方式
<!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>
//想定义一组付款函数,支持三种付款方式: 手机支付, 现金支付,刷卡支付
//手机支付: 不需要给收款员什么东西,而是自己扫二维码
//现金支付: 需要给收款员一笔现金
//刷卡支付: 需要给收款员银行卡和密码两个数据
//都是 "支付"
function pay( ){
// arguments{ }.length=2
// 0 1
//如果没有传入实参就手机支付
if(arguments.length==0){
console.log(`手机支付...`)
}else if(arguments.length==1){
//否则如果传入了1个实参就现金支付
console.log(`现金支付...收您${arguments[0]}元`)
}else{//否则(暂时只考虑三种情况)
console.log(`刷卡支付...从您卡号${arguments[0]}扣款成功!`)
}
}
//调用时:
//其它:
pay(); //0个实参
pay(100); //1个实参
pay("6553 1234","123456"); //2个实参
</script>
</body>
</html>
运行结果:
手机支付...
现金支付...收您100元
刷卡支付...从您卡号6553 1234扣款成功!
四. 匿名函数:
1. 什么是:
定义函数时,不指定函数名的函数
2. 为什么:
节约内存(用完就被立刻释放了,因为该函数没有变量引用,所以将被当做垃圾回收),划分临时作用域
3. 何时: 2种情况:
(1). 如果一个函数只使用一次时,都首选匿名函数。
(2). 如果希望划分临时作用域,避免不同功能之间变量互相污染时
4. 如何: 2种情况:
(1). 绝大多数回调函数,都使用匿名函数——节约内存
a. 因为: 匿名函数用完后,自动释放!
b. 反例: 如果不用匿名函数,回调函数用完无法释放,浪费内存!
(2). 匿名函数自调:
a. 什么是匿名函数自调: 定义一个匿名函数后,立刻调用自己!
b. 何时: 今后几乎所有js代码都要放在匿名函数自调中!
c. 为什么: 避免使用全局变量,造成全局污染!——今后在公司中,禁止使用全局变量!
d. 如何:
(function(){
要执行的任务
})() //创建匿名函数后,立刻执行!
结果: 匿名函数内用过的局部变量不会遗留在全局,而是随匿名函数一起释放了!
强调: 匿名函数包与不包,对功能是没有影响的!但是,对内存影响很大!
示例: 使用匿名函数自调禁止产生全局变量
<!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>
//不希望start变量在完事后,依然留在内存中!
(function(){
//获得当前系统时间
var start=new Date(); //局部变量
//提示: 开始加载页面内容
alert(`开始加载页面内容, at:${start.toLocaleString()}`)
})();
</script>
<script>
//不希望end变量在使用后依然遗留在内存
(function(){
//获得当前系统时间
var end=new Date(); //局部变量
//提示:页面内容加载完成
alert(`页面内容加载完成, at:${end.toLocaleString()}`);
})();
//在弹出两次对话框之后,匿名函数调用结束,在匿名函数外部就无法访问匿名函数内部的局部变量--防止全局污染
console.log(window);
</script>
</body>
</html>
f12 console中,展开window,找不到start变量和end变量,说明释放了
五. 作用域和作用域链:
1. 作用域(scope):
(1). 什么是作用域:
a. 作用: 一个变量的可用范围
b. 本质: 其实作用域也是一个专门保存变量的对象
(2). 为什么: 防止不同范围之间的变量互相污染
(3). 在js中共包括2级作用域:
( js中是没有块/局部作用域的:js中if ,else if, else, while, do while, switch case, for等的{}都不是作用域,这些{}中的变 量,出了{},依然可以使用)
a. 全局作用域:
1). 什么是: 专门保存全局变量的区域/存储空间
2). 什么是全局变量: 在函数function之外用var创建的变量
3). 特点: 随处可用(优点),可反复使用(缺点,极易被污染)!——也是全局变量的特点!
4). 全局作用域的本质: window对象(在浏览器一打开时就自动创建,我们在全局创建的所有变量和函数都保存在window对象中)
b. 函数作用域:
1). 什么是: 专门保存仅函数内可用的局部变量的区域/存储空间
2). 什么是局部变量: 2种:
i. 在函数内用var创建的变量
ii. 形参变量虽然没有var,但是也是局部变量
3). 特点: 仅函数内可用,不可重用!——也是局部变量的特点!
4). 函数作用域的本质: 也是一种专门保存局部变量的对象 (只不过不是始终存在的,而是调用函数时临时创建,调用完自动释放)
i. 每个函数在定义时,函数对象上都包含一个"好友列表"
好友列表中第一个格子暂时空
好友列表中第二个格子指向window
ii. 当调用函数时,会临时创建本次函数调用的函数作用域对象
iii. 函数调用过程中: 函数总是优先使用好友列表中离自己近的函数作用域对象中的局部变量。除非离自己近的函数作用域对象中,没有需要的局部变量,才被迫去全局window中查找变量使用!——就近原则!
iv. 函数调用后,自动释放好友列表中离自己近格子中的地址,等效于释放了本次函数调用所使用的函数作用域对象。等效于将本次使用的所有局部变量一起释放了!
——所以,所有局部变量都不可重用!
c. 总结:
如果函数内自己创建了所需的局部变量,则优先使用局部变量
如果函数内自己没创建所需的局部变量,则去全局找需要的变量使用!
——先局部,后全局!
d. 示例: 判断以下三个程序的输出结果:
var a=10;
function fun(){
var a=100;
a++;
console.log(a)
}
fun();//101
console.log(a);//10
//var a=10;
//如果全局没有a,就说明哪里都找不到a
//如果强行执行a=100,js引擎就会擅自在全局范围创建一个a变量!!!
//js语言一个广受诟病的缺陷
function fun(){
a=100; //不是创建变量,只是想找一个名为a的变量,把100存入变量a中, 因为函数内部没有自己的var a,所以只能去函数外部全局找有没有a变量。结果,在全局找到一个a变量,就将100存入全局变量a变量中
a++; //因为自己没有a变量,所以去全局找a变量,执行++
console.log(a) //因为自己没有a变量,所以去全局找a变量,输出全局变量a的值
}
fun();//101
console.log(a);//101
var a=10;
function fun(a){
a=100;
a++;
console.log(a)
}
fun();//101
console.log(a);//10
2. 作用域链(scope chain):
(1). 什么是作用域链:
函数对象中保存函数调用时所有可用的作用域对象的链式结构
(2). 其实就是上图中的“好友列表”,学名就叫作用域链!
(3). 作用域链保存着: 一个函数可用的所有变量(局部变量和全局变量)
(4). 作用域链控制着: 变量的使用顺序(先局部后全局!)
六. *****闭包(closure):
1. 什么是闭包:
(1). 用法:既重用一个变量,又保护变量不被篡改的一种编程方法
(2). 本质: ?
2. 为什么使用闭包: 全局变量和局部变量都有不可兼得的优缺点
(1). 全局变量:优: 可重用,缺: 随处可以极易被污染
(2). 局部变量:优: 仅函数内可用,不会被污染,缺: 不可重用!
3. 何时:
如果希望让一个函数即可重用一个变量,又保护这个变量不会被篡改,都用闭包!
4. 如何使用: 3步: (不要问什么)
(1). 定义一个外层函数包裹内层函数和要保护的变量
问题: 内层函数成了外层函数内的局部函数,外部不可使用!
(2). 外层函数将内层函数返回到外部!
说明: 内层函数不要起名!因为反正外部接住这个内层函数后,人家还会起新的变量名。
问题:函数只有调用才执行,才能获得返回结果
函数不调用,就不会执行,也不会获得返回结果!
(3). 想使用内层函数的人,必须调用外层函数获得内层函数!才能使用!
5. 示例: 使用闭包保护钱数,并重用钱数
<!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>
//定义一个函数替小孩儿保管零花钱
//小孩儿每花一笔钱,就从总钱数中减去花的钱,提示余额
//问题: 比如有1000元总金额,保存在哪里?
//要么存在全局: 虽然可以实现功能,但是极易被篡改!
//闭包3步:
//1. 定义外层函数包裹要保护的变量和内层函数
function 父母(){
//在外层函数父母内定义要保护的变量
var total=1000;
//2. 外层函数将内层函数返回到外部
//生出 孩子函数
return function(money){
//要么存在局部: 虽然不会被篡改,但是不可重用
total-=money;
console.log(`花了${money}元,还剩${total}元`)
}
}
//强调: 函数只有调用才执行,才能获得返回结果
// 函数不调用,就不会执行,也不会获得返回结果!
//3. 调用者,必须调用外层函数,才能获得函数返回的内层函数。
var 孩子=父母();
//花了100
孩子(100);//希望剩900
//别人写的代码,可能有意或无意的篡改我的全局变量
//total=0;
//又花了100
孩子(100);//希望剩800
//又花了100
孩子(100);//希望剩700
</script>
</body>
</html>
运行结果:
花了100元,还剩900元
花了100元,还剩800元
花了100元,还剩700元
6. 原理:
7. 总结:
(1). 到底什么是闭包: 外层函数的作用域对象,因为受到内层函数的引用和保护,所以称为闭包对象。
(2). 鄙视题: 闭包形成的原因: 外层函数调用后,因为内层函数引用着外层函数的作用域对象,导致外层函数的作用域对象无法释放,形成了闭包!
8. 简化: 理解闭包,只要找3样东西:
(1). 妈妈函数是谁?function 父母(){}
(2). 妈妈函数要保护的局部变量是谁?var total=1000
(3). 妈妈生出的内层函数孩子是谁,孩子最后叫什么名?
var 孩子=function(money){
total-=money;
console.log(`花了${money}元,还剩${total}元`)
}
9. 问题:
问:多次调用外层函数妈妈,两次生出的孩子,是共用一个受保护的变量?还是各自有独立的受保护的变量?互补干扰?
答: 多次调用妈妈,生出的多个内层函数,各自有独立的闭包变量,互不影响。
10. 总结:
(1). 妈妈一次调用生出的1个孩子,多次调用该孩子,这多次调用过程,共用同一个红包
(2). 妈妈多次调用生出的多个孩子,各自有独立的红包,互不干扰!
11. 闭包的缺点:
比普通的函数多占用一块内存空间——外层函数的函数作用域对象
解决: 一旦一个闭包不再使用,应该尽快释放!
如何: 孩子=null
五. 面向对象
一. 什么是面向对象:
1. 什么是对象:
(1). 狭义的解释: 程序中专门保存现实中一个具体事物的属性和功能的程序结构
(2). 广义的解释: js中除五种原始类型(number, string, bool, null, undefined)之外,其余无法直接保存在变量中的数据类型都称为对象。
比如: var arr=[1,2,3]; arr是数组类型的对象,简称数组
var now=new Date() now是日期类型的对象,简称日期/时间对象
var fun=function(){ ... } fun是函数类型的对象,简称函数
... ...
在面向对象中只讨论狭义的自定义对象如何描述现实中的事物。
2.什么是面向对象编程:
程序都是先用对象结构集中存储一个事物的属性和功能,然后再按需调用对象中的属性和功能
3. 为什么:
便于大量数据的管理和维护
4. 何时:
今后,几乎所有的项目,都用面向对象方式开发!
5. 如何:
面向对象三步/三大特点: 封装 继承 多态
二. 封装:
1. 什么是:
创建一个对象集中保存一个事物的属性和功能
2. 为什么:
'便于大量数据的管理和维护
3. 何时:
今后,只要使用面向对象方式编程,都要先创建对象。
4. 如何: 3种:
函数 vs 方法
相同:无论函数还是方法,本质都是function
不同:不属于任何对象的,在对象外创建的function,称为函数;在对象内创建的function,称为方法
(1). 用{}创建一个对象: {}是new Object()的简写
var 对象名={
属性名 : 属性值,
... : ... ,
方法名 : function(){
... this.属性名 ...
}
}
(2). 用new Object()创建: ——很少用
a. 2步:
1). 创建一个空对象: var 对象名=new Object();
2). 强行向新对象中添加新属性和方法:
对象名.属性名=属性值;
对象名.方法名=function(){ ... }
b. 揭示: 对象底层其实也是关联数组
1). 都是名值对儿的集合
2). 都可用["属性名"]访问成员,都可简写为.属性名
计算机我们写“.属性名”,也会被自动翻译为[“属性名”]
. 其实是 [""]
强调: 如果将来属性名不是固定的,来自于其他变量,则既不能写["变量"],又不能写.变量。只能写[变量]
3). 尝试获取对象或关联数组中一个不存在的属性,都不会报错!而是返回undefined
固定套路: 判断一个对象中是否包含某个属性: 强行获取,看是不是undefined
对象.属性名!==undefined 说明包含!否则,说明不包含!
4). 尝试向对象或关联数组中不存在的属性强行赋值,都不会报错!而是自动添加该属性
固定套路: 如何给一个对象添加新属性或新方法: 唯一办法: 强行赋值!
对象.新属性名=新值;
对象.新方法名=function(){ ... }
5). 都可用for in循环遍历每个属性:
for(var 变量 in 对象或关联数组){
//in会依次取出对象或关联数组中每个属性名,自动保存在in前的变量中
}
5. 如何访问对象中的成员:
(1). 访问对象中的属性: 对象名.属性名
(2). 调用对象中的方法: 对象名.方法名()
6. this:
自动获得正在调用当前函数的点.前的对象 的js关键词(自动存在函数作用域对象中)
(1). 需求: 对象自己的方法中的内容,需要随对象自己的属性值动态变化
(2). 错误的解决: 直接在方法中写属性名
a. 结果: 报错: 属性名 is not defined
b. 原因:
1). 对象不在方法的作用域链中
2). 方法默认只能在自己函数内部或全局作用域查找变量,无权擅自进入某个对象中查找属性!
3). 对象的属性又不是全局变量
(3). 正确的但是不好的解决: 在对象的方法中写死"对象名.属性名"
原理: 让程序先找到指定名称的对象,.运算符可让程序进入对象中,读取对象的属性
问题: 紧耦合,外部的对象名改变,也必须手动修改内部写死的对象名。一旦忘记修改内部的写死的对象名,程序立刻就出错!——不好!不便于维护!
a. 什么是this: 自动获得正在调用当前函数的.前的对象 的js关键词(自动存在函数作用域对象中)
b. 何时: 今后只要对象的方法想使用对象自己的属性都必须加"this."
c. 强调: 今后判断this指向哪里,一定不要看它定义在哪儿!只看调用时.前是谁!
7. 示例: 使用{}创建一个对象,并在对象方法中使用this获得对象自己的属性
<!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>
//创建一个学生对象lilei,保存李磊的:
//姓名: Li Lei
//年龄: 11
//保存李磊会做的一件事:
//自我介绍:
var lilei={//不是作用域
//js中只有两种作用域: 全局作用域,函数作用域
//对象中的内容既不是全局,也不是函数作用域
//对象仅仅是一种结构复杂的特殊存储结构而已!
sname:"Li Lei",
sage:11,
intrSelf:function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}//作用域链: ?个格子?
}
console.log(lilei);
//输出李磊的年龄
console.log(`李磊今年:${lilei.sage}`);
//请李磊做自我介绍:
lilei.intrSelf();//this.sname this.sage
// ↓ ↓
// lilei.sname lilei.sage
//过了一年,李磊长了一岁
lilei.sage++;
//再输出李磊的年龄
console.log(`李磊今年:${lilei.sage}`);
//再请李磊做自我介绍:
lilei.intrSelf();//this.sname this.sage
// ↓ ↓
// lilei.sname lilei.sage
var intrSelf=lilei.intrSelf;
intrSelf();// I'm undefined I'm undefined
//其实执行的是window.intrSelf(),intrSelf()中的this指window,因为window中没有sname和sage,所以window.sname和window.sage返回undefined
</script>
</body>
</html>
运行结果:
{sname: "Li Lei", sage: 11, intrSelf: ƒ}
李磊今年:11
I'm Li Lei, I'm 11
李磊今年:12
I'm Li Lei, I'm 12
I'm undefined, I'm undefined
8. 示例: 验证对象底层就是关联数组:
<!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>
//创建lilei
var lilei=new Object();
console.log(lilei);//{}
lilei.sname="Li Lei";
lilei["sage"]=11;
lilei.intr=function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
console.log(lilei);
lilei["intr"]();
//尝试获取lilei的班级
console.log(lilei["className"]);//undefined
//尝试为李磊添加班级属性
lilei.className="初一2班";
console.log(lilei);
//用关联数组
var lilei=[];
lilei.sname="Li Lei";
lilei["sage"]=11;
lilei.intr=function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
console.log(lilei);
lilei.intr();
console.log(lilei.className);//undefined
lilei["className"]="初一2班";
console.log(lilei);
</script>
</body>
</html>
运行结果:
{}
{sname: "Li Lei", sage: 11, intr: ƒ}
I'm Li Lei, I'm 11
undefined
{sname: "Li Lei", sage: 11, className: "初一2班", intr: ƒ}
[sname: "Li Lei", sage: 11, intr: ƒ]
I'm Li Lei, I'm 11
undefined
[sname: "Li Lei", sage: 11, className: "初一2班", intr: ƒ]
9. 示例: 克隆一个对象:
<!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>
//定义函数,可以克隆一个对象
function 克隆(旧对象){
//1. 先创建一个新的空对象
var 新对象={};
//2. 遍历旧对象中每个属性
for(var 旧属性名 in 旧对象){
//3. 每遍历一个属性,就为新对象添加同名的属性,值为旧对象中同名属性值
//取出旧对象中旧属性值
//放入
//新对象中强行添加的同名属性中
新对象[旧属性名]=旧对象[旧属性名]
// ←搬家←
//千万不能加"",因为 旧属性名 是in前的一个变量,变量值每遍历到一个新属性时,获得的属性名都不一样!是变化的!所以不能加""
//也不能用.旧属性名。因为.等效于[""]
}
//返回创建好的新对象
return 新对象;
}
//测试:
var lilei={
sname:"Li Lei",
sage:11
}
//错误:
//var lilei2=lilei;//将lilei变量中保存的旧对象地址复制一份给lilei2变量。结果: lilei2和lilei使用相同的对象地址,引用同一个对象!——不叫克隆!
//正确:
var lilei2=克隆(lilei);
console.log(lilei);
console.log(lilei2);
console.log(lilei==lilei2);//false
//如果==左右都是对象,则==不再比较对象内容,而是比较两个对象的地址是否相同!
//如果返回true,说明克隆失败!因为地址是同一个对象,说明没有多出一个对象
//如果返回false,说明克隆成功!因为地址不同!说明是先后创建的两个对象!
</script>
</body>
</html>
运行结果:
{sname: "Li Lei", sage: 11}
{sname: "Li Lei", sage: 11}
false
10.封装: 3种方式
1. 用{}创建一个对象:
2.用new Object()创建一个对象:
3. 用构造函数反复创建多个相同结构的对象:
(1). 问题: 如果需要反复创建多个相同结构的对象时,用{}代码就很冗余——重复。也不便于维护!
(2). 何时: 今后只要反复创建多个相同结构的对象时,都用构造函数来创建对象
(3). 如何: 2步
a. 先用构造函数定义同一类型多个对象的相同属性结构!
1). 什么是构造函数: 定义同一类型多个对象的相同属性结构的特殊函数
2). function 一类对象的统一类型名(形参1, 形参2,...){
//用this.属性名=xxx的方式,规定将来这类对象都必须有什么属性
//问题: 属性值不能写死的,因为每个对象虽然属性结构相同,但是属性值各不相同!所以,应该用形参变量来传入具体的属性值!
this.属性名1=形参1;
this.属性名2=形参2;
this.方法名=function(){ 将来构造函数中不应该包含方法定义!
...this.属性名...
}
}
b. 再用new反复调用构造函数创建多个相同结构的对象
1). var 对象名=new 类型名(属性值1, 属性值2, ...)
2). 鄙视: new做了几件事: 4件事
i. 创建一个新的空对象
ii. ?
iii. 调用构造函数,传入属性值实参,还自动将构造函数中的this全都指向正在创建的新对象!
结果: 构造函数中每一句话都相当于是给新对象强行赋值,自动添加新该属性。并将参数接住的属性值,保存进新对象的新属性中。
iv. new将新对象的地址保存到=左边的变量中(new有点儿return的作用)
(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>
//想创建两个学生对象: lilei和hmm
//定义所有学生类型的对象统一的属性结构:
// 形参变量永远可以改名,不必和属性名一致
// 但是强烈建议形参名和属性名保持一致
function Student(sname,sage){
//固定所有学生都有sname和sage两个属性,都要会做自我介绍intr()
this.sname=sname;
this.sage=sage;
this.intr=function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
}
//想创建lilei
var lilei=new Student("Li Lei",11);
console.log(lilei);
//想创建hmm
var hmm=new Student("Han Meimei",12);
console.log(hmm);
</script>
</body>
</html>
运行结果:
Student {sname: "Li Lei", sage: 11, intr: ƒ}
Student {sname: "Han Meimei", sage: 12, intr: ƒ}
(5). 构造函数的优点是: 重用对象结构代码
(6). 问题: 如果在构造函数中包含方法定义,则每创建一个对象,都会反复创建完全相同的函数对象的副本,而不是只创建一个所有对象共用!——浪费了内存!
但是确实所有对象都需要相同的方法!怎么办?
(7). 总结: 今后构造函数中就不应该包含方法定义
二. 继承:
1. 什么是继承:
父对象中的成员,子对象无需重复创建,就可直接使用!
2. 何时:
同一类型的所有子对象,都需要相同的方法定义或属性值时,都会用继承的方式来实现。
3. 如何:
(1). (自动,不需要你手写!) 其实当我们定义一个构造函数时,js程序都会自动赠送我们一个原型对象(prototype),也称为该类型下所有孩子的父对象。
(2). 什么是原型对象(prototype):替所有子对象保存共有方法和属性值的父对象
(3). (自动,不需要你手写!) 当new一个新对象时,new的第二步是自动让新子对象继承构造函数的原型对象。自动设置子对象的_ proto 属性指向构造函数的原型对象。
新子对象.proto = 构造函数.prototype
(4). 结果,将来用子对象试图访问一个子对象没有的共有方法或属性时,js程序先在子对象自身内部查找。如果子对象内部没有,js程序会自动延 proto _向父对象(原型对象)中查找是否包含要用的方法或属性。只要在父对象(原型对象)中找到想要的方法,依然可以用"子对象.共有方法名()"调用。看起来就像子对象在用自己的方法一样!
(5). 问题: 构造函数的原型对象开始时是空的!如何向原型对象中添加共有属性和方法?——唯一的办法只有“强行赋值”!
构造函数.prototype.共有方法名=function(){ … }
4. 总结:
今后,只要所有子对象都要共有的方法或属性值,都要集中保存在构造函数的原型对象中!
5. 问题:
问:为什么指同一个原型对象,从构造函数访问时,要用prototype属性,而从子对象访问时却要用_ proto 。构造函数的prototype属性和子对象的 proto 指的到底是不是同一个对象呢?
答: prototype和 proto _,指的是同一个对象。只不过,通过不同辈分的其它对象访问同一个原型对象时,称呼不同!
6. 问题:
问:原型对象中的this指谁?
(1). 错误: 因为构造函数.prototype.共有方法=function(){ … }
所以共有方法中的this指.前的原型对象!
因为: 这句话只是向原型对象中添加函数,并不是调用共有方法。
(2). 正确: 判断this不要看定义在哪儿,应该看何时被谁调用!
因为共有方法迟早会被"子对象.共有方法()"调用。所以,共有方法中的this不确定。但是肯定指将来正在调用这个共有方法的.前的某个子对象。
7. 示例: 为所有学生的原型对象中添加共用的自我介绍方法
<!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>
//构造函数
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
//因为今后所有学生对象都要能自我介绍,所以,应该在学生类型的构造函数的原型对象中强行添加一个intr()方法。供将来所有学生共用!
Student.prototype.intr=function(){
console.log(`I'm ${this.sname}, I'm ${this.sage}`)
}
console.dir(Student);
//想创建lilei
var lilei=new Student("Li Lei",11);
//想创建hmm
var hmm=new Student("Han Meimei",12);
console.log(lilei);
console.log(hmm);
lilei.intr();
hmm.intr();
//亲自鉴定:
//判断李磊的爹是不是构造函数妈妈的原型对象
console.log(lilei.__proto__==Student.prototype);//true
//判断hmm的爹是不是构造函数妈妈的原型对象
console.log(hmm.__proto__==Student.prototype);//true
//判断lilei的爹和hmm的爹是不是同一个人
console.log(lilei.__proto__==hmm.__proto__);//true
</script>
</body>
</html>
8. 自有属性和共有属性:
(1). 什么是:
a. 自有属性: 保存在当前对象内部,仅归当前对象自己所有的属性
b. 共有属性: 保存在原型对象(父对象)中,归多个子对象共有的属性
(2). 获取属性值: 无论自有属性,还是共有属性,都可用"子对象.属性名",用法是相同的!
(3). 修改属性值:
a. 自有属性: 应该用子对象直接修改。子对象.自有属性名=属性值
b. 共有属性:
1). 错误: 用子对象.共有属性方式=新值
因为: 共有属性归所有子对象共用,如果允许某一个子对象擅自篡改,则牵一发而动全身!——危险!
所以: js禁止子对象修改原型对象中的共有属性
但是: 如果强行用子对象.共有属性=新值,js程序会自动给这个子对象添加一个自有的同名属性保存新属性值。而不会影响原型对象中的共有属性
结果: 从此,这个子对象,在这个共有属性的使用上,与其他子对象,分道扬镳!
2). 正确: 共有属性,只能用原型对象修改: 构造函数.prototype.共有属性=新值
示例: 为李磊和hmm添加共有属性className,存储二人共同的班级名,并用正确和错误的方式分别修改共有属性className,查看结果:
<!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>
//构造函数
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
//所有学生都来自初一2班,所以应该在原型对象中添加一个className属性保存所有学生统一的班级
Student.prototype.className="初一2班";
//创建lilei
var lilei=new Student("Li Lei",11);
//创建hmm
var hmm=new Student("Han Meimei",12);
console.log(lilei);
console.log(hmm);
//读取李磊的姓名和班级
console.log(lilei.sname, lilei.className);
//读取hmm的姓名和班级
console.log(hmm.sname, hmm.className);
//李磊想改名叫李晓磊
lilei.sname="Li Xiaolei";
console.log(lilei);
//第一年,输出两个人的班级
console.log(lilei.className, hmm.className);
//过了一年,两个人同时升级到初二2班
//错误: 用某个一个子对象,试图修改原型对象中的共有属性值
lilei.className="6年级2班";
//正确:
//Student.prototype.className="初二2班";
//再输出两个人的班级,希望两人都是初二2班
console.log(lilei.className, hmm.className);
//又过了一年,大家一起升初三2班
Student.prototype.className="初三2班";
console.log(lilei.className, hmm.className);
</script>
</body>
</html>
运行结果:
Student {sname: "Li Lei", sage: 11}
Student {sname: "Han Meimei", sage: 12}
Li Lei 初一2班
Han Meimei 初一2班
Student {sname: "Li Xiaolei", sage: 11}
初一2班 初一2班
6年级2班 初一2班
6年级2班 初三2班
9. 内置对象的原型对象:
(1). 什么是内置类型/内置对象: ECMAScript标准中规定的,浏览器已经实现的对象或类型。
(2). 11种:
String Number Boolean
Array Date RegExp Math(不是类型,已经是一个对象了)
Error
Function Object
Global(对象)(在浏览器中被window代替)
(3). 什么是类型: (其实除了Math和global之外,其余9种都是类型,都由两部分组成)
构造函数+原型对象形成的整体
a. 构造函数负责: 创建该类型的子对象。构造函数中的所有属性会成为将来子对象内的自有属性
b. 原型对象负责: 集中保存该类型所有子对象公共的成员(属性值和方法)。原型对象中的方法和属性值都会成为将来所有子对象的共有成员。
(4). 问题: 我怎么知道数组家共有哪些函数?
只要看类型.prototype中有哪些函数即可!
(5). 问题: 如果经常对数组执行一种操作,但是原型对象中没有提供现成的函数可用,怎么办?——只要自定义一个函数,强行添加到数组的原型对象中即可!
Array.prototype.新函数名=function(){
//this->今后调用这个函数的.前的某个子对象
}
结果: 将来任何一个数组都可以.新函数名()
(6). 示例: 为数组类型添加一个对所有元素求和的共有函数
<!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>
//项目中,经常需要对各种数组内容求和
//应该自定义一个sum函数,强行添加到数组类型的原型对象中
Array.prototype.sum=function(){
console.log(`调用我自己定义的sum()函数!`);
//this->将来调用这个sum()函数的.前的某个数组
//比如,将来arr1.sum(),this->arr1
// 将来arr2.sum(), this->arr2
//对当前正在调用sum()的某个数组中所有元素求和
var result=0;
//遍历数组中每个元素
for(var i=0;i<this.length;i++){
result+=this[i]
}
//错误: in会把sum()函数本身的定义也遍历进来
// for(var key in this){
// result+=this[key]
// }
return result;
}
console.log(Array.prototype);
//希望:
var arr1=[1,2,3];
var arr2=[1,2,3,4,5];
console.log(arr1.sum())//6; //报错: sum不是一个函数
console.log(arr2.sum())//15
</script>
</body>
</html>
运行结果:
[sum: ƒ, constructor: ƒ, concat: ƒ, ...]
调用我自己定义的sum()函数!
6
调用我自己定义的sum()函数!
15
10. 原型链:
(1). 什么是原型链: 由多级父对象,逐级继承,形成的链式结果
(2). 原型链保存着一个对象可用的所有属性和方法
(3). 原型链控制着属性和方法的使用顺序:先自有,再共有——就近原则
(4). 问题: 不同类型的子对象,调用toString(),输出的结果格式千差万别!
三. 多态:
1. 什么是多态:
一个函数,不同情况下,表现出不同的状态!
2. 其实多态分两种情况:
(1). 重载(overload): 一个函数,根据传入的实参值不同,执行不同的逻辑!(已学过)
(2). 重写(override):
3. 什么是重写:
在子对象中定义一个和父对象中成员同名的成员!
4. 为什么:
因为从爹继承来的东西,不总是好用的!
5. 何时重写:
今后,只要从爹继承来的东西不好用!就用重写自己的!
6. 如何:
只要在当前子对象中定义一个和父对象中不好用的成员同名的新成员即可。
7. 结果:
根据就近原则,优先使用子对象自己定义的。不再使用父对象中不好用——推翻
8. 问题:
问:不同类型的子对象,调用toString(),输出的结果格式千差万别?
答: 所有内置类型的原型对象中,为了自己家孩子着想,已经重写了适合自己家孩子的toString()
9. 问题:
问: 但是,咱们自己定义的类型中还没有重写toString()方法,所以非常不便于调试对象的内容!
解决: 强烈建议所有自定义类型和自定义对象,都要重写自定义的toString()方法,便于调试!
10. 示例: 为自定义类型重写toString()方法
<!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>
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
//为了让Student家的孩子们,也有好用的toString(),需要在Student的原型对象中,添加一个自定义的好用的toString()
Student.prototype.toString=function(){
//字符串格式和内容可以自定义
return `{
学生姓名: ${this.sname},
学生年龄: ${this.sage}
}`
}
var lilei=new Student("Li Lei",11);
var arr1=new Array(1,2,3);
var now=new Date();
//分别输出函数,lilei,arr1和now的toString()
console.log(Student.toString());
console.log(lilei.toString());
console.log(arr1.toString());
console.log(now.toString());
</script>
</body>
</html>
运行结果:
{
学生姓名: Li Lei,
学生年龄: 11
}
1,2,3
Fri Jul 03 2020 18:11:06 GMT+0800 (中国标准时间)
面向对象总结:
1. 封装: 先创建对象: 2种:
(1). 如果只创建一个对象: {}
(2). 如果反复创建多个相同结构的对象: 构造函数
2. 继承: 所有子对象共用的方法和属性值,必须集中定义在原型对象中
3. 多态: 如果父对象继承来的成员不好用!可以重写自己的!
补:
for in 循环,禁止用于遍历索引数组!只能遍历关联数组或对象
因为: in不但遍历当前对象的每个属性(包括数字下标和非数字下标),且会自动延_ _proto_ _继续遍历父对象中的未隐藏的成员!
四. 自定义继承
1. 问题:如果整个父对象都不是想要的!
解决: 其实可以修改一个对象的父对象——换爹
2. 如何: 2种:
(1). 只更换一个对象的父对象: 其实就是修改对象的_ _proto_ _继承新的父对象
a. 子对象._ _proto_ _=新父对象
b. 问题: 有些浏览器不允许擅自修改_ _proto_ _
解决: Object.setPrototypeOf(子对象, 新父对象) —— 今后建议
设置原型对象的子对象(为)新父对象
c. 示例: 仅更换hmm一个对象的爹
<!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>
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
var lilei=new Student("Li Lei",18);
var hmm=new Student("Han Meimei",19);
var father={
bal:100000000000,
car:"infiniti"
}
console.log(lilei);
console.log(hmm);
//hmm修改自己的_ _proto_ _属性指向father
// hmm.__proto__=father;
Object.setPrototypeOf(hmm, father);
//比较:
console.log(hmm.bal, hmm.car);
console.log(lilei.bal, lilei.car);
</script>
</body>
</html>
运行结果:
(2). 更换所有子对象的父对象:
a. 其实就是更换构造函数的prototype属性:
构造函数.prototype=新父对象
b. 时机: 必须在创建子对象之前就要更换!
c. 示例: 同时更换lilei和hmm的父对象
<!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>
function Student(sname,sage){
this.sname=sname;
this.sage=sage;
}
var father={
bal:100000000000,
car:"infiniti"
}
//在生lilei和hmm之前就换原型对象:
Student.prototype=father;
var lilei=new Student("Li Lei",18);
var hmm=new Student("Han Meimei",19);
console.log(lilei);
console.log(hmm);
//结果:
console.log(lilei.bal, lilei.car);
console.log(hmm.bal, hmm.car);
</script>
</body>
</html>
运行结果:
六.ES5: ECMAScript标准的第五个版本
一. 严格模式:
- 什么是严格模式: 比普通的js运行机制要求更严格的新的运行机制。
- 为什么: js语言本身有很多广受诟病的缺陷
- 何时使用严格模式: 今后几乎所有的js程序都必须运行在严格模式下
- 如何启用严格模式: 在当前作用域的顶部: “use strict”;
启用 严格 - 新规定:
(1). 禁止给未声明过的变量赋值:
a. 旧js中: 给未声明的变量赋值,不报错,而是在全局自动创建该变量
b. 缺点: 极容易造成全局污染和歧义!
c. 启用严格模式: 禁止给未声明过的变量赋值——报错!
d. 好处: 大大减少了全局污染的可能!也避免了部分拼写错误!便于调试
e.示例: 尝试给未声明的变量赋值:
<!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>
"use strict";//启用严格模式
function send(){
var gf;
//想给我的女朋友发一条私信
//gf="今晚308,w84u";
//不小心错发给了前女友
qgf="今晚308,w84u";//严格模式下,会报错!避免了误会发生
console.log(gf);
}
send();
//公开!
console.log(qgf);
//console.log(gf);//报错!
</script>
</body>
</html>
运行结果:
Uncaught ReferenceError: qgf is not defined
at send (3_use_strict.html:17)
at 3_use_strict.html:20
(2). 静默失败升级为错误:
a. 什么是静默失败: 程序执行不成功,但也不报错!
b. 缺点: 极其不便于程序的调试
c. 启用严格模式: 几乎将所有静默失败都升级为了错误
d. 好处: 便于程序调试!
e. 示例: 演示静默失败
<!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>
"use strict";
var eric={
eid:1001,
ename:"埃里克"
};
//要求员工的编号不能修改!
//设置eric对象的eid属性为只读
Object.defineProperty(eric,"eid",{
writable:false
});//先不要问,稍后重点讲
//试图修改eric的eid属性
eric.eid=-2; //报错:
//Cannot assign to read only property 'eid'
// 不能 赋值 给 只读 属性 eid
console.log(eric);
</script>
</body>
</html>
运行结果:
Uncaught TypeError: Cannot assign to read only property 'eid' of object '#<Object>'
at 4_use_strict.html:24
(3). 普通函数调用中的this不再指向window,而是undefined
a. 旧js中: 普通函数中的this默认指window
b. 缺点: 极大的增加了全局污染的概率!
c. 启用严格模式: 普通函数调用中的this不再指向window,而是undefined。
d. 好处: this后不能随便加.xxx,加上就报错!有效的防止了因为this造成的全局污染!
e. 示例: 演示错误的使用构造函数
<!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>
"use strict";
function Student(sname,sage){
this.sname=sname;//报错: Cannot set property 'sname' of undefined
//this->undefined,不能加.sname,加上就错!
this.sage=sage;
//没有return
}
var lilei=new Student("Li Lei",11);
//错误的使用了构造函数:
var hmm=Student("Han Meimei",12);
//this->window
//this.sname -> window.sname="Han Meimei"
//this.sage -> window.sage=12
console.log(lilei);
console.log(hmm);
console.log(sname);//全局
console.log(sage);//全局
console.log(window);
</script>
</body>
</html>
运行结果:
Uncaught TypeError: Cannot set property 'sname' of undefined
at Student (5_use_strict.html:13)
at 5_use_strict.html:20
(4). 禁用arguments.callee
a. 什么是arguments.callee: 是在一个函数内自动获得当前函数本身 的关键词
b. 何时: 递归调用时
c. 为什么: 如果在递归调用时,在函数内写死函数名,是非常不好的
因为: 紧耦合,万一外部函数名发生了变化,总要记得修改内部写死的函数名,一旦忘记修改内部写死的函数名,程序立刻出错!
d. 所以: 将来就算用递归算法,也不能在函数内写死函数名。应该用arguments.callee代替写死的函数名,自动获得当前函数本身。与函数名无关了!
e. 好处: 松耦合: 从此即使修改外部的函数名,函数内部一点都不用修改!保持不变即可!
f. 为什么禁用: 其实是不推荐使用递归调用。
g. 递归调用的问题: 重复计算量太大!
h. 解决: 绝大多数的递归都可以用循环来解决——难度极高!
i. 总结:
1). 如果使用递归算法可以轻松解决问题,又不影响效率,完全可以首选递归算法。只不过不要用arguments.callee了!依然在函数中写死函数名。
2). 只有用递归算法非常影响效率时,才被迫寻找循环算法代替递归。
二. 保护对象:
阻止别人的程序对我们自己的对象执行不合理的非法的修改操作。
1. 保护属性
(1). 问题: js中的对象毫无自保能力!外界的程序可以随意修改对象中的属性
(2). 解决:
a. ES5标准中,每个对象中的每个属性,都变成一个缩微的小对象
b. 每个属性小对象中,都包含四个更小的属性:
1). value: 为当前属性保存属性值
2). writable: 开关,控制是否可修改当前属性值
3). enumerable: 开关,控制着是否可用for in遍历到该属性
强调: 只防for in,防不住.点 ——半隐藏,浅紫色
4). configurable: 开关,控制2件事
i. 控制着是否可删除当前属性
ii. 控制着是否可修改前两个开关
强调: configurable开关,一旦改为false!不可逆!任何其它程序,都无权重新打开开关!除非修改源代码!
(3). 如何修改开关的值来限制对属性的操作:
a. 错误: 细微的开关属性,不能用.直接访问!
b. 只能用专门的函数:
1). 只修改一个属性中的多个开关:
(重新)定义 属性
i. Object.defineProperty(对象名, "属性名",{
开关名: true或false
... : ...
});
ii. 问题: 我们关上的开关,别人可以随意再打开——相当于没有保护!
iii. 解决: 今后只要修改writable和enumerable开关时,都要同时设置configurable:false,作为双保险!禁止任何程序再打开开关!
iv. 问题: Object.defineProperty()一次只能修改一个对象中的属性。如果对象中有很多属性都需要保护,则反复写Object.defineProperty()太麻烦了!
2). 同时修改多个属性的多个开关
a. Object.defineProperties(对象名,{
属性名:{
开关: true或false,
... : ...
},
属性名:{
开关: true或false,
... : ...
},
... ...
})
示例: 使用defineProperty()操作属性的开关
<!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>
"use strict";
var eric={
eid:1001,
ename:"埃里克",
salary:12000
}
//系统要求:
//eid只读,
Object.defineProperty(eric,"eid",{
writable:false,
configurable:false //不可逆 //双保险
});
//ename禁止删除,
Object.defineProperty(eric,"ename",{
configurable:false
})
//salary禁止遍历
Object.defineProperty(eric,"salary",{
enumerable:false,
configurable:false //双保险
})
//试图重新打开被关闭的writable开关!
// Object.defineProperty(eric,"eid",{
// writable:true,
// configurable:true //不可逆
// });//报错: Cannot redefine property: eid
// // 不能 重新定义 属性 eid
// //试图修改eid
// eric.eid=-2; //报错: Cannot assign to read only property 'eid'
//试图删除ename
//delete eric.ename; //报错: Cannot delete property 'ename'
//试图遍历eric所有属性(包括salary)
for(var key in eric){
console.log(`属性名: ${key}, 属性值:${eric[key]}`)
}
console.log(eric);
</script>
</body>
</html>
运行结果:
属性名: eid, 属性值:1001
属性名: ename, 属性值:埃里克
{eid: 1001, ename: "埃里克", salary: 12000}
<!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>
"use strict";
var eric={
eid:1001,
ename:"埃里克",
salary:12000
}
//系统要求: eid只读, ename禁止删除, salary禁止遍历
Object.defineProperties(eric,{
eid:{
writable:false,
configurable:false
},
ename:{
configurable:false
},
salary:{
enumerable:false,
configurable:false
}
})
//试图重新打开被关闭的writable开关!
// Object.defineProperty(eric,"eid",{
// writable:true,
// configurable:true //不可逆
// });//报错: Cannot redefine property: eid
// // 不能 重新定义 属性 eid
//试图修改eid
//eric.eid=-2; //报错: Cannot assign to read only property 'eid'
//试图删除ename
//delete eric.ename; //报错: Cannot delete property 'ename'
//试图遍历eric所有属性(包括salary)
for(var key in eric){
console.log(`属性名: ${key}, 属性值:${eric[key]}`)
}
console.log(eric);
//用.强行访问salary
console.log(`eric.salary=${eric.salary}`)
</script>
</body>
</html>
运行结果:
属性名: eid, 属性值:1001
属性名: ename, 属性值:埃里克
{eid: 1001, ename: "埃里克", salary: 12000}
eric.salary=12000
(4). 问题: 开关只能机械的保护一种情况,无法灵活的设定自定义的保护条件
比如: 员工的年龄,可以修改,但是值必须介于18~65之间!
(5). 解决: 访问器属性
a. 什么是访问器属性: 不实际保存属性值,仅提供对其它数据属性的保护 的特殊属性——保镖!
b. 何时: 如果需要用自定义的规则限制用户对属性的操作时,都要用访问器属性
c. 如何: 2步:
1). 先定义一个隐姓埋名且半隐藏的数据属性实际保存数据:
2). 请保镖:
i. 保镖的名称,必须冒名顶替要保护的属性——迷惑外界
ii. 保镖一请就是一对儿: 名字都固定了:get和set
iii. 因为保镖可以执行验证和加工等操作,所以两个保镖应该都是函数
其中: get函数,专门负责从受保护的数据属性中读取属性值
set 函数,专门负责接收新值,并保存到受保护的数据属性上——修改
d. 外界如何使用访问器属性:
1). 前提: 外界不知道有受保护的属性,也不知道自己访问的是访问器属性,更不知道还有get和set函数。外界只知道有一个属性可以获取和修改值,仅此而已。
2). 当外界试图获取属性值时,访问器属性会自动调用get函数,从受保护的属性中取出值
3). 当外界试图修改属性值时,访问器属性会自动调用set函数,并将要修改的新值先传给set函数的value形参,先验证。如果新值验证通过才由set函数保存到受保护的变量中。如果验证不通过,则可以报错,且不保存非法的属性值!
e. 访问器属性中的this为什么不指访问器属性本身,而指当前访问器属性所在对象?
因为访问器属性书写时,是放在一个{}中,但是,一旦进入对象,就被打散了!get和set两个函数直接隶属于所在的对象,并且与受保护的数据属性平级!
所以get和set中的this,指当前get/set函数直接隶属于的对象!
示例: 使用访问器属性保护对象的年龄属性
<!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>
var eric={
ename:"埃里克",
eage:25
}
//eric的eage属性值可以改,但必须介于18~65之间
Object.defineProperties(eric,{
//1. 先添加一个半隐藏的_eage属性,实际保存原eage属性的值
_eage:{
//将原eric对象的eage属性值搬家到_eage中隐藏起来
value:eric.eage,
//因为eage是可以修改:
writable:true,
//因为_eage应该半隐藏:
enumerable:false,
//双保险:
configurable:false
},
//2. 将原eage属性替换为保镖:
eage:{//冒名顶替原属性名,迷惑外界
//保镖一请就是一对儿:
get:function(){//保镖
console.log(`自动调用了一次eage内的get(),返回${this._eage}`);
//专门负责从受保护的_eage中取出现在的属性值
return this._eage;
},
set:function(value){//保镖//value将来会自动接住要修改的新值
console.log(`自动调用一次eage中的set(),形参value=${value}`);
//专门负责接收新值,验证后,保存到受保护的数据属性中
if(value>=18&&value<=65){
console.log(`验证通过,保存到_eage中`)
this._eage=value;
}else{
console.log(`验证不通过,报错!`)
throw Error("年龄超范围!必须介于18~65之间!")
}
},
//访问器属性应该代替受保护的属性抛头露面
enumerable:true,
//访问器属性不能随意被删除,双保险
configurable:false
//访问器属性已经没有value和writable:
//因为访问器属性自己不实际保存属性值,所以没有value属性
//因为writable不好用!太单一!所以我们才被迫使用访问器属性!所以是访问器属性代替了开关writable的作用。所以访问器属性中也不再需要writable开关了!
}
})
//外界不知道有_eage,get和set,只知道有一个eage属性保存年龄
//外界试图获取年龄时: 会发生什么事儿?自动调用get
console.log(eric.eage);
//外界试图修改年龄时: 会发生什么事儿?自动调用set
eric.eage=26;
console.log(eric);
//外界试图修改年龄为非法的-2,会发生什么事儿?
eric.eage=-2;
</script>
</body>
</html>
运行结果:
(6). 问题: 无论用开关,还是访问器属性,都是在保护单个属性,无法防住对对象结构的破坏!比如:防不住别人恶意为对象添加新属性
2. 保护结构:
防止别人的程序擅自篡改对象的属性结构!3个级别:
(1). 防扩展: 禁止给对象添加新属性:
Object.preventExtensions(对象)
阻止 扩展
问题: preventExtensions()只防添加,不防删除,如果一个对象所有属性都禁止删除,就必须给每个属性手工添加configurable:false——太繁琐了
(2). 密封: 既禁止扩展,又禁止删除属性
a. Object.seal(对象)
密封
b. 原理:
1). 既自动调用preventExtensions()
2). 又自动设置所有属性的configurable:false!
c. 所以: 一般使用了seal(),就不用preventExtensions()了
d. 结论: 今后,绝大多数对象,保护到seal级别就够了!
e. 示例: 使用防扩展和密封保护对象结构
<!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>
"use strict";
var eric={
eid:1001,
ename:"埃里克"
}
Object.defineProperty(eric,"eid",{
writable:false
});
//防扩展:
//Object.preventExtensions(eric);
// 阻止 扩展
//密封:
Object.seal(eric);
//尝试修改eric的eid
//eric.eid=-2;
//试图为eric添加新属性
//eric.Eid=-2;//报错:
//Cannot add property Eid, object is not extensible
// 不能 添加 属性 Eid(因为)对象是 不 可以扩展的
//尝试删除ename
//delete eric.ename;//报错:
//Cannot delete property 'ename'
console.log(eric);
</script>
</body>
</html>
(3). 冻结: 既禁止扩展,又禁止删除,甚至禁止修改所有属性值!
a. Object.freeze(对象);
b. 原理: 3件事:
1). 既自动调用preventExtensions()
2). 又自动设置所有属性的configurable:false
3). 又自动修改所有属性的writable:false
c. 何时: 绝大多数情况下,属性值还是应该可以修改的!所以不用冻结!但是,如果一个对象被多个模块同时使用,最好禁止修改属性值。否则,任由其中一个模块擅自修改共用对象的属性值,一定会牵一发而动全身!
d. 示例: 冻结一个对象:
<!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>
"use strict";
var pool={
ip:"127.0.0.1",
port:3306,
db:"xz"
};
//希望pool中的所有属性值禁止修改
Object.freeze(pool);
//试图修改pool的ip属性值
pool.ip="192.168.0.100";//报错: Cannot assign to read only property 'ip'
</script>
</body>
</html>
运行结果:
Uncaught TypeError: Cannot assign to read only property 'ip' of object '#<Object>'
at 11_freeze.html:20
3.总结
严格模式: "use strict";
(1). 禁止给未声明过的变量赋值
(2). 静默失败升级为错误
(3). 普通函数调用中的this不指window,而是指undefined
(4). 禁用arguments.callee
保护对象:
(1). 保护属性:
a. 每个属性包含三个开关:
1). writable: 控制是否可修改属性值
2). enumerable: 控制着是否可被for in遍历到,但是只防for in不防.
3). configurable: 控制
i. 是否可删除当前属性
ii. 是否可修改writable和enumerable两个开关
强调: configurable一旦改为 false,不可逆!
b. 只修改一个属性的多个开关:
Object.defineProperty(对象名, "属性名",{开关: true/false})
c. 修改多个属性的多个开关:
Object.defineProperties(对象名,{
属性名:{ 开关:true/false, ... },
... : ...
})
d. 如果用自定义的规则保护属性时,只能用访问器属性: 2步:
Object.defineProperties(对象,{
//1). 先定义一个隐姓埋名且半隐藏的数据属性:
_属性名:{
value: 属性的初始值,
writable:true,
enumerable:false,
configurable:false
},
//2). 再定义访问器属性保镖冒名顶替要保护的属性
属性名:{
get:function(){
return this._属性名
},
set:function(value){ //value ← 要修改的新属性值
先验证value
如果验证通过,this._属性名=value
否则如果验证未通过,不但不保存新属性值,还会报错
},
enumerable:true,
configurable:false
}
})
外界试图获取访问器属性值时,自动调用get()
外界试图修改访问器属性值时,自动调用set()
(2). 保护结构: 3个级别
a. 防扩展: Object.preventExtensions(对象)
b. 密封: Object.seal(对象)
c. 冻结: Object.freeze(对象)
总结: this 4种: 判断this,一定不要看定义在哪儿!只看调用时!
1. obj.fun() this->obj
2. fun() 或 (function(){ ... })() this->window
3. new Fun() this->new正在创建的新对象
4. 类型名.prototype.共有方法=function(){ ... } this->将来谁调用指谁,同第一种情况
三. Object.create():
- 问题: 如果在没有构造函数的情况下,也想创建一个子对象,继承父对象
- 解决: Object.create()
- 如何: 一句话三件事:
var 子对象=Object.create(父对象,{
//语法和defineProperties相同
属性名:{
开关:true或false,
... : ...
},
... : {
...
}
})
-
原理:
- 先创建一个新的空对象
- 自动让新对象继承指定的父对象
- 为子对象添加自有属性
-
示例: 使用Object.create()创建子对象,继承父对象
<!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>
var father={
bal:100000000000,
car:"infiniti"
};
//创建一个新对象lilei,并让李磊自动继承father对象
var lilei=Object.create(father,{
//为李磊添加自有属性sname和sage
sname:{
value:"Li Lei",
writable:true,
enumerable:true
},
sage:{
value:11,
writable:true,
enumerable:true
}
});
//密封李磊对象
Object.seal(lilei);
console.log(lilei);
</script>
</body>
</html>
运行结果:
1.{sname: "Li Lei", sage: 11}
1.sage: 11
2.sname: "Li Lei"
3.__proto__:
1.bal: 100000000000
2.car: "infiniti"
__proto__: Object
四. 替换this
- 问题: 有时函数中的this指向的对象,不是我们想要的!
- 解决: 其实如果函数中的this指向的对象不是我们想要的,我们是可以修改的!
- 第一种: 只在本次调用函数时,临时修改一次this指向的对象
(1). 要调用的函数.call(替换this的对象, 实参值, ...)
(2). 2件事:
a. 调用一次函数执行,同时传入实参值
b. 同时临时替换函数中的this为()中第一个实参值指定的对象
示例: 定义公共计算器函数,用不同对象替换函数中的this
<!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 奖金2
function jisuan(base,bonus1,bonus2){
console.log(`${this.ename} 的总工资是: ${base+bonus1+bonus2}`)
}
//有两个员工:
var lilei={ ename:"Li Lei" };
var hmm={ ename:"Han Meimei" };
//两个员工都想用jisuan()函数,计算自己的薪资,都希望计算结果中显示自己的名字
//比如: lilei想调用jisuan()函数,计算自己的薪资,希望计算结果中显示自己的名字"Li Lei"
//其实就是希望把jisuan()函数中的this->lilei
//错误做法1:
/* *///jisuan(10000, 1000, 2000)//this->window
//错误做法2:
//lilei.jisuan(10000,1000,2000)//报错: lilei.jisuan is not a function
//一个对象的原型链上保存着这个对象可用的所有成员
//李磊的原型链:
//lilei自己内部没有jisuan()
//lilei的爹也没有jisuan()
//console.log(lilei.__proto__);
//lilei的爷爷是null了,所以更没有jisuan()
//console.log(lilei.__proto__.__proto__);
//结论: jisuan()函数虽然在全局,但是却没有在lilei的原型链上!所以lilei.jisuan()找不到,就会报错!
//正确: //*临时*替换this为lilei
jisuan.call( lilei,10000, 1000, 2000);
// | ↓ ↓ ↓
//function jisuan( ↓ base, bonus1, bonus2){
// this.ename...
//为什么是临时的:
jisuan(10000,1000,2000);//this->window
//hmm也想调用jisuan()函数,计算自己的薪资
jisuan.call( hmm, 4000, 5000, 6000);
// | ↓ ↓ ↓
//function jisuan( ↓ base, bonus1, bonus2){
// this.ename...
</script>
</body>
</html>
运行结果:
Li Lei 的总工资是: 13000
undefined 的总工资是: 13000
Han Meimei 的总工资是: 15000
- 第二种情况: 只在本次调用函数时,临时修改一次this指向的对象,并打散数组传参
(1). 问题: 有时,多个实参值是放在一个数组中整体给我们的!而函数需要的却是分散的多个实参值!出现了不一致
(2). 解决: 用apply()代替call()
(3). 如何: 要调用的函数.apply(替换this的对象, 数组)
(4). 原理:
a. 调用一次函数执行,同时先打散数组参数为多个实参值,再传入
b. 同时临时替换函数中的this为()中第一个实参值指定的对象
示例: 如果李磊的薪资是放在一个数组中整体给的,如何替换this并传参?
<!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 奖金2
function jisuan(base,bonus1,bonus2){
console.log(`${this.ename} 的总工资是: ${base+bonus1+bonus2}`)
}
//有两个员工:
var lilei={ ename:"Li Lei" };
var hmm={ ename:"Han Meimei" };
//李磊的三个薪资是放在一个数组中给的
var arr=[10000,1000,2000];
//错误做法:
//jisuan.call(lilei,arr/*,undefined,undefined*/)
// | ↓ ↓ ↓
//function jisuan( ↓ base, bonus1, bonus2)
// this.ename
// [10000,1000,2000]+undefined+undefined
//自动将数组转为字符串,拼接字符串——不是我们想要的!
//正确做法: 先打散数组为多个值,再传参
jisuan.apply(lilei, arr )
// | 打散数组
// | 10000, 1000, 2000
// | ↓ ↓ ↓
//function jisuan( ↓ base, bonus1, bonus2)
// this.ename
</script>
</body>
</html>
运行结果:
Li Lei 的总工资是: 13000
- 第三种情况: 基于原函数,创建一个新函数副本,并永久绑定this为指定对象
(1). 问题: 如果一个函数需要反复调用,但是每次都被迫用call()或apply()替换this,极其不方便!
(2). 解决: 完全可以"买"一个一模一样的新函数副本,在新函数副本中永久绑定想用的this!
(3). 如何: var 新函数名=原函数.bind(替换this的对象)
(4). 原理:
a. 创建一个和原函数一模一样的新函数!不是调用函数!
b. 永久的替换新函数中this为指定的对象
(5). 问题: 如果每次调用函数时,个别实参值也是固定的!每次都需要传入固定的个别实参值——很麻烦的!
(6). 解决: 其实bind不但可以永久替换this,而且还能永久绑定个别实参值
var 新函数名=原函数.bind(替换this的对象, 固定实参值, ...)
(7). 原理:
a. 创建一个和原函数一模一样的新函数!不是调用函数!
b. 永久的替换新函数中this为指定的对象
c. 永久替换对应位置的形参变量为一个固定的值!
示例: 创建一个新的计算器函数,永久绑定this为lilei,base为10000
<!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 奖金2
function jisuan(base,bonus1,bonus2){
console.log(`${this.ename} 的总工资是: ${base+bonus1+bonus2}`)
}
//有两个员工:
var lilei={ ename:"Li Lei" };
var hmm={ ename:"Han Meimei" };
//李磊觉得总是跟别人共用一个,总是借用很不方便!
//且李磊的底薪每个月都是固定的!不想每次都输入10000
// jisuan.call(lilei,10000,1000,2000);
// jisuan.call(lilei,10000,2000,2000);
// jisuan.call(lilei,10000,1000,1500);
//于是lilei决定买一个新的一模一样的,自己专属的计算器
var jisuan2=jisuan.bind(lilei,10000);
// | ↓永久
// jisuan2:function( | base,...){
//↓ base=10000
//this->lilei
// console.log(`${lilei.ename} 的总工资是: ${base+bonus1+bonus2}`)
//}
//从此李磊使用计算2时,再不用call了!再不用借了!
//且每个月计算薪资时不用重复输入底薪了
jisuan2(1000,2000);
jisuan2(2000,2000);
jisuan2(2000,1500);
//结论: 每次调用时即使不传入lilei,也能输出lilei说明是永久的替换this!即使不传入实参值10000,也能将10000累加到总工资,说明base也永久绑定为10000了!
</script>
</body>
</html>
运行结果:
Li Lei 的总工资是: 13000
Li Lei 的总工资是: 14000
Li Lei 的总工资是: 13500
- 总结: 3种:
(1). 在一次调用函数时,临时替换this,首选: 函数.call(对象, 实参值,...)
(2). 临时替换一次this,但是需要打散数组再传参时,被迫改为:
函数.apply(对象, 数组)
(3). 创建一个一模一样的新函数并永久绑定this和部分实参值:
var 新函数名=原函数.bind(对象, 固定实参值, ...)
五.数组新函数:
1. 判断:
(1).every:
(1). every: 判断数组中是否所有元素都符合要求
a. var 判断结果=arr.every(function(元素值, 下标, 当前数组){
return 根据元素值,下标和当前数组,判断当前元素是否符合要求
})
说明: 如果回调函数中的判断条件用不到后两个实参值,可以省略后两个形参。
b. 原理:
1). every中自带for循环,自动遍历.前的数组中每个元素
2). 每遍历一个元素,就自动调用一次回调函数
3). 每次调用回调函数时,都自动传入三个值:
i. 第一个实参值value: 自动收到当前正在遍历的元素值
ii. 第二个实参值i: 自动收到当前正在遍历的元素位置
iii. 第三个实参值arr: 自动收到当前every.前的数组对象
4). 回调函数中用传入的三个值验证当前正在遍历的元素是否符合条件的要求!并将判断结果返回给every()函数
5). 2种情况:
i. 如果本次回调函数返回的true,说明当前元素符合要求!于是every会继续向后遍历,知道遍历结束。如果遍历结束,所有元素的验证结果都返回true,则整个every就返回true,说明整数数组中所有元素都符合要求
ii. 如果本次回调函数返回false!说明当前元素不符合要求!every就没必要继续循环!而是立刻退出执行。整个every()返回false,说明数组不是所有元素都符合要求!
<!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>
var arr1=[1,2,3,4,5]; //false
var arr2=[2,4,6,4,2]; //true
//判断哪个数组全由偶数组成:
// 三个变量名都能换
// 但是顺序不能变
// 当前元素值 下标 数组
// ↓ ↓ ↓
var result1=arr1.every(function(value, i, arr){
console.log(`arr1.every()自动调用了一次回调函数;自动传入arr1数组:[${arr}]的第${i}个元素值:${value}。经过回调函数验证,返回${value%2==0}`)
//每个元素是否都是偶数
return value%2==0;//当前元素值是否为偶数
});
var result2=arr2.every(function(value, i , arr){
console.log(`arr2.every()自动调用了一次回调函数;自动传入arr2数组:[${arr}]的第${i}个元素值:${value}。经过回调函数验证,返回${value%2==0}`)
return value%2==0;//当前元素值是否为偶数
});
console.log(result1, result2);
</script>
</body>
</html>
运行结果:
arr1.every()自动调用了一次回调函数;自动传入arr1数组:[1,2,3,4,5]的第0个元素值:1。经过回调函数验证,返回false
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第0个元素值:2。经过回调函数验证,返回true
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第1个元素值:4。经过回调函数验证,返回true
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第2个元素值:6。经过回调函数验证,返回true
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第3个元素值:4。经过回调函数验证,返回true
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第4个元素值:2。经过回调函数验证,返回true
false true
(2). some:
(2). some: 判断数组中是否包含符合要求的元素
a. var 判断结果=arr.some(function(元素值, 下标, 当前数组){
return 根据元素值,下标和当前数组,判断当前元素是否符合要求
})
b. 原理:
1). some中自带for循环,自动遍历.前的数组中每个元素
2). 每遍历一个元素,就自动调用一次回调函数
3). 每次调用回调函数时,都自动传入三个值:
i. 第一个实参值value: 自动收到当前正在遍历的元素值
ii. 第二个实参值i: 自动收到当前正在遍历的元素位置
iii. 第三个实参值arr: 自动收到当前some.前的数组对象
4). 回调函数中用传入的三个值验证当前正在遍历的元素是否符合条件的要求!并将判断结果返回给some()函数
5). 2种情况:
i. 如果回调函数当前元素的验证结果返回true,说明当前元素符合要求,则some()立刻退出循环,不再继续执行。而且整个some()返回true,说明当前数组包含至少一个符合要求的元素。
ii. 如果回调函数当前元素的验证结果返回false,说明当前元素不符合要求,则some()会继续向后执行!如果所有元素的验证结果都为false,说明数组中并不包含任何一个符合要求的元素。则整个some()返回false!
示例: 使用some函数,判断哪个数组包含偶数:
<!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>
var arr1=[1,2,3,4,5]; //true 2次
var arr2=[2,4,6,4,2]; //true 1次
//判断哪个数组包含偶数:
// 三个变量名都能换
// 但是顺序不能变
// 当前元素值 下标 数组
// ↓ ↓ ↓
var result1=arr1.some(function(value, i, arr){
console.log(`arr1.every()自动调用了一次回调函数;自动传入arr1数组:[${arr}]的第${i}个元素值:${value}。经过回调函数验证,返回${value%2==0}`)
//每个元素是否都是偶数
return value%2==0;//当前元素值是否为偶数
});
var result2=arr2.some(function(value, i , arr){
console.log(`arr2.every()自动调用了一次回调函数;自动传入arr2数组:[${arr}]的第${i}个元素值:${value}。经过回调函数验证,返回${value%2==0}`)
return value%2==0;//当前元素值是否为偶数
});
console.log(result1, result2);
</script>
</body>
</html>
运行结果:
arr1.every()自动调用了一次回调函数;自动传入arr1数组:[1,2,3,4,5]的第0个元素值:1。经过回调函数验证,返回false
arr1.every()自动调用了一次回调函数;自动传入arr1数组:[1,2,3,4,5]的第1个元素值:2。经过回调函数验证,返回true
arr2.every()自动调用了一次回调函数;自动传入arr2数组:[2,4,6,4,2]的第0个元素值:2。经过回调函数验证,返回true
true true
2. 遍历:
(1). forEach: 单纯简化for循环!
a. arr.forEach(function(value, i, arr){
对value,i或arr执行操作
})
b. 原理:
1). forEach中自带for循环,自动遍历.前的数组中每个元素
2). 每遍历一个元素,就自动调用一次回调函数
3). 每次调用回调函数时,都自动传入三个值:
i. 第一个实参值value: 自动收到当前正在遍历的元素值
ii. 第二个实参值i: 自动收到当前正在遍历的元素位置
iii. 第三个实参值arr: 自动收到当前forEach.前的数组对象
4). 回调函数中对当前元素值,数组中当前位置的元素执行相同的操作,不需要返回值
c. 为什么: for循环已经没有进一步简化的空间了!且每次都写for循环,太麻烦了!
d. 问题: forEach是数组家的函数,只有遍历索引数组时,才能使用forEach。如果遍历类数组对象,虽然也是数字下标,但是不能使用forEach。
示例: 使用forEach简化遍历数组中每个元素
<!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>
var arr=["亮亮","然然","东东"];
//遍历花名册中每个人名,点到谁,谁喊到!
//for循环
for(var i=0;i<arr.length;i++){
alert(`${arr[i]} - 到!`)
}
//forEach
// arr.forEach(function(value){
// alert(`${value} - 到!`)
// })
//forEach+箭头函数(暂时不要问!后边详细讲)
//arr.forEach((value)=>{alert(`${value} - 到!`)})
//箭头函数,如果只有一个形参,可省略()
//arr.forEach(value=>{alert(`${value} - 到!`)})
//箭头函数,如果只有一句函数体,可省略{}
//arr.forEach(value=>alert(`${value} - 到!`))
//不愿意写那么长单词
arr.forEach(v=>alert(`${v} - 到!`))
</script>
</body>
</html>
(2). map: 遍历出原数组中每个元素值,加工后,放入新数组中返回!
a. var 新数组=arr.map(function(value, i, arr){
return 加工后的新元素值
})
b. 原理:
1). arr.map()先创建一个空数组等待
2). map()也自带for循环自动遍历原数组中每个元素
3). 每遍历到一个元素,就自动调用一次回调函数
4). 每次调用回调函数时,都自动传入三个值:
i. 第一个实参值value: 自动收到当前正在遍历的元素值
ii. 第二个实参值i: 自动收到当前正在遍历的元素位置
iii. 第三个实参值arr: 自动收到当前map.前的数组对象
5). 回调函数中根据当前传入的元素值等信息,加工出一个新的元素值返回给map()
6). map()会将收到的新元素值放入新数组中相同位置保存起来
7). 遍历结束,map()会将新数组返回出来!——原数组保持不变
c. 何时: 今后只要希望保护原数组不变,返回新数组供其它位置的代码使用时
示例: 对数组中每个值*2,返回新数组,原数组保持不变
<!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>
var arr=[1,2,3,4,5];
//想对数组中每元素*2,返回新数组,但是,想保持原数组不变!
var arr2=arr.map(function(value,i,arr){
console.log(`arr.map()自动调用了一次回调函数。将原数组[${arr}]中${i}位置的旧值${value},传入回调函数。经过回调函数加工,返回新元素值${value*2},自动放入新数组${i}位置`);
//想把当前元素值*2后的新值放入新数组中?
return value*2;
});
console.log(arr2);
console.log(arr);
</script>
</body>
</html>
运行结果:
arr.map()自动调用了一次回调函数。将原数组[1,2,3,4,5]中0位置的旧值1,传入回调函数。经过回调函数加工,返回新元素值2,自动放入新数组0位置
arr.map()自动调用了一次回调函数。将原数组[1,2,3,4,5]中1位置的旧值2,传入回调函数。经过回调函数加工,返回新元素值4,自动放入新数组1位置
arr.map()自动调用了一次回调函数。将原数组[1,2,3,4,5]中2位置的旧值3,传入回调函数。经过回调函数加工,返回新元素值6,自动放入新数组2位置
arr.map()自动调用了一次回调函数。将原数组[1,2,3,4,5]中3位置的旧值4,传入回调函数。经过回调函数加工,返回新元素值8,自动放入新数组3位置
arr.map()自动调用了一次回调函数。将原数组[1,2,3,4,5]中4位置的旧值5,传入回调函数。经过回调函数加工,返回新元素值10,自动放入新数组4位置
(5) [2, 4, 6, 8, 10]
(5) [1, 2, 3, 4, 5]
3. filter过滤:
复制出原数组中符合要求的元素值放入新数组中返回,原数组保持不变!
(1). var 新数组=arr.filter(function(value,i,arr){
return 判断条件
})
(2). 原理:
a. filter()先创建一个空数组等待
b. filter ()也自带for循环自动遍历原数组中每个元素
c. 每遍历到一个元素,就自动调用一次回调函数
d. 每次调用回调函数时,都自动传入三个值:
1). 第一个实参值value: 自动收到当前正在遍历的元素值
2). 第二个实参值i: 自动收到当前正在遍历的元素位置
3). 第三个实参值arr: 自动收到当前filter.前的数组对象
e. 在回调函数中判断当前传入的元素值是否符合要求,并返回判断结果
f. 如果本次函数调用结果返回true,说明当前元素符合要求,则filter()自动将当前元素追加到新数组中保存。如果本次函数调用结果返回false,说明当前元素不符合要求,则filter()本次什么也不做,而是继续遍历下一个元素。
g. 遍历结束,filter()将新数组返回
示例: 过滤出数组中偶数位置的元素,放入新数组中返回
<!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>
var arr=[1,2,3,4,5];
//希望过滤出数组中的偶数,放入新数组返回
var arr2=arr.filter(function(value,i,arr){
console.log(`arr.filter自动调用了一次回调函数。将原数组${arr}中${i}位置的元素值${value}传入回调函数。经过判断返回${value%2==0}。`)
//判断当前元素是否为偶数
return value%2==0
});
console.log(arr2);
console.log(arr);
</script>
</body>
</html>
运行结果:
arr.filter自动调用了一次回调函数。将原数组1,2,3,4,5中0位置的元素值1传入回调函数。经过判断返回false。
arr.filter自动调用了一次回调函数。将原数组1,2,3,4,5中1位置的元素值2传入回调函数。经过判断返回true。
arr.filter自动调用了一次回调函数。将原数组1,2,3,4,5中2位置的元素值3传入回调函数。经过判断返回false。
arr.filter自动调用了一次回调函数。将原数组1,2,3,4,5中3位置的元素值4传入回调函数。经过判断返回true。
arr.filter自动调用了一次回调函数。将原数组1,2,3,4,5中4位置的元素值5传入回调函数。经过判断返回false。
(2) [2, 4]
(5) [1, 2, 3, 4, 5]
4. 汇总:
遍历数组中每个元素,经过求和或其他汇总方式,统计出一个最终结论
(1). var 结果=arr.reduce(
function(临时汇总值, 当前元素值, 下标, 当前数组){
return 临时汇总值和当前元素值计算出的新临时汇总值
} ,
起始值
)
(2). 原理:
a. reduce()先用一个变量保存住起始值,遍历过程中,也可保存临时汇总值。
b. reduce()自带for循环,自动遍历原数组中每个元素
c. 每遍历一个元素就自动调用一次回调函数
d. 每次调用回调函数时,都传入保存四个值:
1). 第一个实参值box: 自动传入截止到目前的临时汇总值
2). 第二个实参值value: 自动收到当前正在遍历的元素值
3). 第三个实参值i: 自动收到当前正在遍历的元素位置
4). 第四个实参值arr: 自动收到当前reduce.前的数组对象
e. 回调函数内,可将当前元素的值,汇总到临时汇总值中,形成新的汇总值,并返回给reduce()
f. reduce()接到回调函数返回的新临时汇总值之后,会覆盖之前旧的临时汇总值保存在变量中,为继续汇总下一个元素值做准备!
g. 遍历结束,最后的临时汇总值,就是整个数组中所有元素的最终汇总值。
示例: 计算数组中所有元素的和
<!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>
var arr=[1,2,3,4,5];
//想对数组内容求和
var result=arr.reduce(
function(box,value,i,arr){
console.log(`arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=${box},和当前元素值${value},返回box+value的新汇总值${box+value}`)
return box+value
},
0//从0开始累加
);
console.log(result);
</script>
</body>
</html>
运行结果:
arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=0,和当前元素值1,返回box+value的新汇总值1
arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=1,和当前元素值2,返回box+value的新汇总值3
arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=3,和当前元素值3,返回box+value的新汇总值6
arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=6,和当前元素值4,返回box+value的新汇总值10
arr.reduce()自动调用了一次回调函数。传入截止目前的临时汇总值box=10,和当前元素值5,返回box+value的新汇总值15
15
七. ES6:
一. 模板字符串:
1. 什么是模板字符串: 支持换行,支持动态生成内容的新字符串
2. 为什么: 旧js中,想拼接字符串,都用+,但是+极容易和算数运算的+产生歧义
3. 何时: 今后几乎所有的字符串拼接都用模板字符串,不再用+
4. 如何:
(1). 整个字符串用一对儿反引号` ... `包裹
(2). 模板字符串内需用动态生成的内容,必须放在${}中
5. ${}中: 将来VUE框架中绑定语法和这里的规定完全一样!
(1). 能放什么?变量,运算,三目,调用方法,创建对象,访问数组中元素
总结: 只要有返回值的合法的变量或js表达式,都可放在${}中
(2). 不能放什么?不能放程序结构(分支和循环),也不能放没有返回值的js表达式
示例: 使用模板字符串拼接各种字符串
<!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>
var price=12.5;
var count=5;
console.log(`
单价: ¥${price.toFixed(2)}
数量: ${count}
===========================
小计: ¥${(price*count).toFixed(2)}
`);
var sex=1;
console.log(`性别:${sex==1?"男":"女"}`);
//复习***第一阶段***日期:
//获得当前时间对应的ms数
//new Date().getTime()
var orderTime=1594085622575;
//将ms数转为人能看懂的日期
console.log(`下单时间:${new Date(orderTime).toLocaleString()}`);
var week=["日","一","二","三","四","五","六"];
// 0 1 2 3 4 5 6
//获得当前日期星期几对应的数字(从0开始的!星期日是一周的第一天0)
var i=new Date().getDay();//2
console.log(`今天星期${week[i]}`);
</script>
</body>
</html>
运行结果:
单价: ¥12.50
数量: 5
===========================
小计: ¥62.50
性别:男
下单时间:2020/7/7 上午9:33:42
今天星期二
二. let
1. 什么是: 专门代替var用于声明变量的新关键词
2. 为什么: var的问题:
(1). 会被声明提前——打乱程序正常的执行顺序
(2). 没有块级作用域——代码块内的变量,很有可能影响块外的代码!
块级作用域: if,else,for,while,do while,switch等这些程序结构的{},在js中都不是作用域!这些{}防不住内部的var变量被声明提前到{}外部。即使在{}内声明的变量,出了{}依然可以使用!
3.示例: 演示var 的两个缺点:
<!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>
var t=0;//定义全局变量t,用于累加每个函数的执行时间
function fun1(){
console.log(`fun1执行耗时0.3s`);
t+=0.3;
}
function fun2(){
//var t;// 因为fun2中已经有了局部变量t
console.log(`fun2执行耗时0.8s`);
t+=0.8; //所以0.8不会加到全局变量!而是加给局部变量t。所以,最后全局变量t少了0.8s
//将来可能会追加一段新的代码
if(false){//不会执行!但是if的{不是作用域,防不住内部的var变量被声明提前!
var t=new Date();
console.log(`新代码在${t.toLocaleString()}被执行`)
}
}
fun1();
fun2();
console.log(`共耗时${t}s`);//1.1s
</script>
</body>
</html>
运行结果:
fun1执行耗时0.3s
fun2执行耗时0.8s
共耗时0.3s
4. 何时: 今后所有的变量都要用let声明!
5. let的好处:
(1). 不会被声明提前——保证程序顺序执行
(2). 让代码块也变成了作用域!——保证块内的变量,无法超出块的范围影响外部!
6. 原理: let底层会悄悄的做两件事,来避免let的变量和外部变量同名!
(1). let会被自动翻译为匿名函数自调,有自己的作用域
(2). let的变量会被悄悄改名,避免重名——双保险!
7. 示例: 使用let避免不同范围之间的变量互相干扰:
<!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>
var t=0;//定义全局变量t,用于累加每个函数的执行时间
function fun1(){
console.log(`fun1执行耗时0.3s`);
t+=0.3;
}
function fun2(){
//var t;// 因为fun2中已经有了局部变量t
console.log(`fun2执行耗时0.8s`);
t+=0.8; //所以0.8不会加到全局变量!而是加给局部变量t。所以,最后全局变量t少了0.8s
//将来可能会追加一段新的代码
if(false){//不会执行!但是if的{不是作用域,防不住内部的var变量被声明提前!
var t=new Date();
console.log(`新代码在${t.toLocaleString()}被执行`)
}
}
fun1();
fun2();
console.log(`共耗时${t}s`);//1.1s
</script>
</body>
</html>
运行结果:
fun1执行耗时0.3s
fun2执行耗时0.8s
共耗时0.3s
8. let小脾气:
(1). 在全局let的变量,竟然在window里找不到!
(2). 在相同作用域内,不允许用let同时声明两个同名的变量
(3). 禁止提前使用let声明的变量
(4). 相同代码块中只有let的变量受控范围不会超出块的范围。其他如果有var声明的变量,照样超出块的范围,影响外部。----let并没有让块真的变成作用域,而仅仅用匿名函数自调用将自己变量限制起来。
9. 示例: 演示 let的小脾气:
<!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>
var a=10;
let b=100;
// (function(){
// var b=100;
// })();
console.log(window);
let c=10;
//let c=100;//报错:
//Identifier 'c' has already been declared
//变量名/标识符 已经 被 声明
//console.log(d); //报错
//Cannot access 'd' before initialization
//不能 访问 d 在xxx前 初始化(首次声明并赋值)
let d=100;
</script>
</body>
</html>
运行结果:
三. 箭头函数:
1. 什么是: 专门简写function的定义函数的新写法
2. 为什么: 就是因为到处写function!太烦人!
3. 何时: 今后几乎所有function,都可被箭头函数代替——今后在项目中应该几乎看不到function才对!
4. 如何: 3句话:
(1). 去掉function,在()和{}之间加=>
(2). 如果形参列表只有一个形参,则可以省略()
强调: 如果没有形参,则必须加()
(3). 如果函数体只有一句话!可以省略{}
强调:
a. 唯一的一句话结尾,一定不要加分号!
b. 如果仅剩的一句话,还是return,则必须去掉return!
5. 示例: 使用箭头函数简写普通函数
<!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>
// function add(a,b){
// return a+b;
// }
// console.log(add(3,5));//8
var add=(a,b)=>a+b;
console.log(add(3,5));//8
// var arr=[23,12,2,1,123,3];
// arr.sort(function(a,b){return a-b});
// console.log(arr);
var arr=[23,12,2,1,123,3];
arr.sort((a,b)=>a-b);
console.log(arr);
// var str="you can you up";
// str=str.replace(/\b[a-z]/g,function(keyword){
// return keyword.toUpperCase();
// });
// console.log(str);
var str="you can you up";
str=str.replace(
/\b[a-z]/g,keyword=>keyword.toUpperCase());
console.log(str);
// var arr=["亮亮","然然","东东"];
// arr.forEach(function(value){
// console.log(`${value} - 到`)
// });
var arr=["亮亮","然然","东东"];
arr.forEach(value=>console.log(`${value}-到`));
// var arr=[1,2,3,4,5];
// var arr2=arr.map(function(value){
// return value*2;
// });
// console.log(arr2);
var arr=[1,2,3,4,5];
var arr2=arr.map(value=>value*2);
console.log(arr2);
// var arr=[1,2,3,4,5];
// var result=arr.reduce(function(box,value){
// return box+value
// },0);
// console.log(result);//15
var arr=[1,2,3,4,5];
var result=arr.reduce((box,value)=>box+value,0);
console.log(result);//15
// (function(){
// var t=new Date();
// console.log(`网页加载完成,at:${t.toLocaleString()}`)
// })();
(()=>{
var t=new Date();
console.log(`网页加载完成,at:${t.toLocaleString()}`)
})();
// var s=5;
// var timer=setInterval(function(){
// console.log(s);
// s--;
// if(s==0){
// console.log(`boom!!!`);
// clearInterval(timer);
// }
// },1000)
var s=5;
var timer=setInterval(()=>{
console.log(s);
s--;
if(s==0){
console.log(`boom!!!`);
clearInterval(timer);
}
},1000)
</script>
</body>
</html>
运行结果:
8
(6) [1, 2, 3, 12, 23, 123]
You Can You Up
亮亮-到
然然-到
东东-到
(5) [2, 4, 6, 8, 10]
15
网页加载完成,at:2020/7/7 下午12:05:01
5
4
3
2
1
boom!!!
6. 箭头函数特点:
(1). 问题: 回调函数中的this,通常都指window
因为: 回调函数在主函数中被调用时,前边往往什么都没有!
(2). 解决: 将普通的function改为箭头
(3). 原理: 箭头函数内的this,自动与箭头函数外的this保持一致!
7. 示例: 使用箭头函数分别简写对象方法和回调函数
<!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>
//this->window
var lilei={//本来就不是作用域,不是实体墙!它只是new Object()的简写,属于全局作用域
sname:"Li Lei",
friends:["亮亮","然然","东东"],
//intr:()=>{//错误:intr内this->对象外this->window
//intr:function(){
intr(){//ES6对对象方法的简写: 可以不写":function"
//强调: ES6简写绝对不等于箭头函数,因为不影响this!this还和使用function时保持一致!
//intr内this->将来lilei.intr()中.前的lilei
//希望李磊会说:
//Li Lei认识 亮亮
//Li Lei认识 然然
//Li Lei认识 东东
//遍历李磊自己的friends数组
//如果前边intr改为=>,则这里报错:
//不能访问undefined的forEach
//this.friends=undefined
//因为this.friends=window.friends=undefined
this.friends.forEach(
//function(value){//实体墙
//问题: 希望this指lilei,但是回调函数的this->window
//解决: 将function改为箭头函数
value=>{//栅栏
//结果: 回调函数内部this->外部intr内的this->lilei
//仅限于this,内部的其它局部变量依然不会超出函数的范围
console.log(`${this.sname} 认识 ${value}`)
}
)
}
}
lilei.intr();
</script>
</body>
</html>
运行结果:
Li Lei 认识 亮亮
Li Lei 认识 然然
Li Lei 认识 东东
8. 总结:
(1). 今后,如果函数中没用到this,或刚好希望内外this保持一致时,才能用箭头函数简写!
(2). 今后,如果不希望内外this相同时!是不能用箭头函数简写!
比如: 对象的方法不能用箭头函数简写(应该用ES6方法简写去掉":function")
DOM中的事件处理函数,也不希望函数内的this与外部一致(将来讲...)
面试题:箭头函数与普通函数的区别:高频面试题
(1)箭头函数中的this与外部的this保持一致。但是普通函数的this与外部的this无关
(2)箭头函数不能作为构造函数,解决(class)
(3)箭头函数中不允许使用arguments
四. for of
1. 什么是: 单纯简化普通for循环,遍历索引数组和类数组对象
2. 为什么:
(1). 传统的for循环已经没有简化的空间
(2). forEach又只能让数组使用,类数组对象用不了!
3. 何时: 今后只要遍历索引数组和类数组对象时都可用for of
4. 如何: 元素值
↓
for(var 变量 of 数组/类数组对象){
... ...
}
其中: of会依次取出每个元素的值,保存到of前的变量中
5. 总结:
6. for of不能做:
(1). 不能获得下标位置
(2). 无法倒序遍历,或调整遍历的步长,只能从头到尾挨个遍历
(3). 也不能单纯数数,而只能遍历一个有length属性的数组或类数组对象。
7. 结论: 因为绝大多数循环,都不需要关心下标位置,不需要倒序,不需要调整步调,也不需要数数,所以for of反而比for循环和forEach用的都多!
8. 示例: 使用for of简化for循环,遍历各种东西
<!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>
var arr=["亮亮","然然","东东"];
//3种:
// for(var i=0;i<arr.length;i++){
// console.log(`${arr[i]}-到!`)
// }
// arr.forEach(function(value){
// console.log(`${value}-到!`)
// })
// arr.forEach(v=>console.log(`${v}-到!`));
for(var v of arr){console.log(`${v}-到!`)}
function add(){
var sum=0;
// for(var i=0;i<arguments.length;i++){
// sum+=arguments[i];
// }
//类数组对象
//arguments.forEach(v=>sum+=v);//报错
//arguments.forEach is not a function
for(var v of arguments){sum+=v}
return sum;
}
console.log(add(1,2,3));//6
console.log(add(1,2,3,4,5));//15
var str="hello";
// 01234.length=5
for(var i=0;i<str.length;i++){//顺序遍历
console.log(str[i]);
}
for(var i=str.length-1;i>=0;i--){//倒叙遍历
console.log(str[i]);
}
for(var i=0;i<str.length;i+=2){//调整遍历的步长
console.log(str[i]);
}
// for(var v of str){
// console.log(v);
// }
for(var i=0;i<5;i++){
console.log(i);
}
// for(var i of 5){ //报错: 5 is not iterable不可遍历
// console.log(i)
// }
</script>
</body>
</html>
运行结果:
亮亮-到!
然然-到!
东东-到!
6
15
h
e
2 l
o
o
2 l
e
h
h
l
o
0
1
2
3
4
五. 参数增强: 3个新技能
- 参数默认值:
(1). 什么是: 定义函数时为参数提供一个默认值,如果将来调用函数时,没有提供实参值,形参变量也有默认值使用
(2). 何时: 只要希望将来即使不传入实参值,形参变量也有默认值可用时
(3). 如何: 定义函数时:
function 函数名(形参1, ... , 最后一个形参=默认值){
//如果将来调用函数时,没有传入最后一个实参值,则最后一个形参变量也有默认值可用!
}
(4). 结果:
a. 调用时,传入了实参值,则形参变量采用传入的实参值
b. 调用时,没有传入实参值,则形参变量自动采用提前准备好的默认值
(5). 局限: 通常参数默认值只能解决最后一个形参变量不确定有没有值的情况!
如果中间某个形参不确定有没有,又想有默认值,不能用参数默认值实现的!
因为将来调用函数传参时,是不可能隔着蹦着传参的!
(6). 解决: 任意一个参数不确定,都能有默认值使用呢?!(稍后讲...)
(7). 示例: 使用参数默认值保证即使没有传入实参值,形参变量也有默认值可用
<!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>
//定义一个函数,显示我的自我介绍!
function intr(msg="主人很懒,什么也没有留下"){
console.log(`我的自我介绍是:${msg}`)
}
//正常情况,如果传入了msg
intr("you can you up!");
//调用intr时,没有传入任何实参值
intr();
//定义一个点套餐的函数
function order(zhushi="香辣鸡腿堡",xiaochi="薯条",yinliao="可乐"){
console.log(`您点的餐是:
主食:${zhushi},
小吃:${xiaochi},
饮料:${yinliao}
`)
}
//第一个人不着急,套餐中每个东西都自己定义
order("香辣鸡腿堡","薯条","可乐");
//第二个人着急赶火车,没空挨个选择,想要套餐原装的内容
order();
//第三个人只想把最后一个可乐换成咖啡,前两项不变!
//order(,,"咖啡");//语法错误!
//第四个人只想换第二个薯条为菠萝派,主食和饮料保持不变
//order(,"菠萝派")//语法错误!
</script>
</body>
</html>
运行结果:
我的自我介绍是:you can you up!
我的自我介绍是:主人很懒,什么也没有留下
您点的餐是:
主食:香辣鸡腿堡,
小吃:薯条,
饮料:可乐
您点的餐是:
主食:香辣鸡腿堡,
小吃:薯条,
饮料:可乐
- 剩余参数:
(1). 问题: ES6中箭头函数中禁止使用arguments!
(2). 解决: 用ES6中的剩余参数语法代替arguments
(3). 什么是剩余参数: 专门代替arguments来获得不确定个数的实参值的新语法
(4). 何时: 今后只要在es6代码中,遇到不确定参数个数的情况,都用剩余参数语法代替arguments
(5). 如何: 定义函数时:
function 函数名(形参1, 形参2, ...数组名){
//...数组名,在将来调用函数时,会自动收集除前两个形参之外的,多余的所有剩余参数值,保存进一个自定义名称的数组中!
}
(6). 剩余参数的数组和arguments的差异:
a. 类型:
1). 剩余参数的数组是纯正的数组类型,可以使用数组家所有的函数
2). arguments是类数组对象,不是数组家孩子,所以不能使用数组家函数
b. 获取哪些实参值:
1). 剩余参数的数组之前可以定义其它形参,而剩余参数的数组仅获取其它形参不要的多余的剩余参数值
2). arguments只能获得所有实参值!不能有所选择!
(7). 示例: 用剩余参数代替arguments实现求和、计算总工资的功能
<!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>
//function add(){
//用箭头函数简写add函数
// 可以改名
var add=(...arr)=>{
//...收集, arr是数组名
//(将剩余实参值都)收集到arr(数组中保存)!
//用for of:
//var sum=0;
//for(var v of arguments){//报错: arguments is not defined——箭头函数中不允许使用arguments了!
//for(var v of arr){
// sum+=v;
//}
//用reduce
var sum=arr.reduce((box,v)=>box+v,0);
return sum;
}
//var add=(...arr)=>arr.reduce((box,v)=>box+v,0);//语法糖
console.log(add(1,2,3));//6
console.log(add(1,2,3,4,5));//15
//定义一个计算总工资的函数
//用户至少输入自己的员工姓名,但是每个员工的工资项目数不一样
// 必须 其它剩余
function jisuan(ename,...arr){
console.log(arguments);//含员工姓名
console.log(arr);//不含员工姓名
console.log(`${ename}的总工资是:${
arr.reduce((box,v)=>box+v,0)
}`);
}
jisuan("Li Lei",10000,1000,2000);
// ename ...arr[10000,1000,2000]
jisuan("Han Meimei",1000,2000,3000,4000,5000)
// ename ...arr[1000,2000,3000,4000,5000]
</script>
</body>
</html>
运行结果:
6
15
Arguments(4) ["Li Lei", 10000, 1000, 2000, ... ]
(3) [10000, 1000, 2000]
Li Lei的总工资是:13000
Arguments(6) ["Han Meimei", 1000, 2000, 3000, 4000, 5000, ...]
(5) [1000, 2000, 3000, 4000, 5000]
Han Meimei的总工资是:15000
- 打散数组:
(1). 问题: apply(),虽然可以打散数组再传参,但是apply()的主要作用是替换this,捎带着打散数组。而实际开发中,有很多情况只单纯打散数组再传参,不需要替换this!用apply(),就很不方便!
(2). 解决: 今后只要单纯打散数组,再传参时,与this无关时,都首选ES6的打散数组语法
(3). 如何: 调用函数时:
函数(...数组名)
打散!
(4). 总结: 定义函数时...是收集,调用函数时...是打散
(5). 示例: 使用Math.max和Math.min获取一个数组中的最大值和最小值
<!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>
console.log(Math.max(2,7,1,5));//7
console.log(Math.min(2,7,1,5));//1
var arr=[2,7,1,5];
//想获取数组中的最大值和最小值,又不愿意自己写遍历!
//console.log(Math.max(arr)); //NaN max不支持数组!
//尝试用apply先打散数组,再传给Math.max()
console.log(
// 替换this的对象, 要打散的数组
Math.max.apply( null , arr) //错!
//因为本例和this无关
//所以第一个实参值,填什么都行!!!——别扭!
);
//ES6的spread语法: 先打散arr数组,再传参
console.log(Math.max(...arr));//7
console.log(Math.min(...arr));//1
</script>
</body>
</html>
运行结果:
7
1
7
7
1
(6). 其实: ...有很多极其简洁的语法糖!
<!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>
//复制一份数组:
var arr=[1,2,3];
var arr2=[...arr];
//[]创建新空数组
//...arr 先打散原数组,再传值,保存进新数组
console.log(arr2);
console.log(arr==arr2);//false
//合并两个数组或多个数组
var arr1=[1,2,3];
var arr2=[5,6,7];
var arr3=[...arr1,4,...arr2,8];
console.log(arr3);
//克隆一个对象:
var lilei={
sname:"Li Lei",
sage:11
};
var lilei2={...lilei};
//{}创建空对象
//...lilei:先打散李磊对象为多个属性,再传入新对象中保存
console.log(lilei2);
console.log(lilei2==lilei);//false
//合并两个对象或多个对象以及属性值
var obj1={x:1,y:2};
var obj2={m:4,n:5};
var obj3={
...obj1,
z:3,
...obj2,
o:6
};
console.log(obj3);
</script>
</body>
</html>
运行结果:
(3) [1, 2, 3]
false
(8) [1, 2, 3, 4, 5, 6, 7, 8]
{sname: "Li Lei", sage: 11}
false
{x: 1, y: 2, z: 3, m: 4, n: 5, …}
六. 解构(destruct):
从一个大的数组或对象中仅提取出个别想要的值,单独使用!3种:
- 数组解构: 从一个数组中,仅提取出想要的个别元素单独使用
(1). [变量1, 变量2 ,...]=数组
0 1 ...
(2). 说明=左边不是真正的数组,仅仅是装扮成和等号右边相同的数组的样子。=一旦发现左右两边的结构是一样的,就会将右边数组中相同位置的元素值自动复制给=左边相同位置的变量
(3). 结果: 变量1=数组[0]——数组0位置的元素
变量2=数组[1]——数组1位置的元素
(4). 简写:
var 变量1, 变量2;
[变量1, 变量2]=arr
可简写为: var [变量1, 变量2]=arr;
(5). 解构不连续位置上的元素值:
[变量1, , 变量2, ...]=arr
0 1 2
结果: 变量1=arr[0]; 变量2=arr[1];
(6). 按值传递: 解构也是给变量赋值,同样遵守按值传递
a. 如果数组中保存的是原始类型的值,则解构时,变量得到的是值的副本,将来修改变量值,不会影响原数组中原始值。
b. 如果数组中保存的是引用类型的值,则解构时,变量得到的是元素对象的地址值,将来通过变量修改对象内容时,同样影响原数组中元素对象。
(7). 示例: 使用数组解构,解构出数组中对应位置的元素值,单独使用
<!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>
var products=[
{pname:"小米",price:3488},
{pname:"华为",price:5488},
{pname:"苹果",price:8488},
{pname:"三星",price:6488},
{pname:"VIVO",price:2488},
{pname:"OPPO",price:1488}
];
//只想用前三个商品对象,暂时不用后两个商品对象:
//var p1,p2,p3;
var [p1,p2,p3]=products;
console.log(p1); //地址
p1.price-=1000; //修改变量引用的对象
console.log(products[0]); //跟着改变
console.log(p2);
console.log(p3);
var arr=[1,2,3,4,5,6];
//var a,b,c;
var [a,b,c]=arr;
console.log(a);//1 //副本
a-=1; //修改变量
console.log(arr[0]);//保持不变
//如果向要第1个,第3个,第6个商品
//var p1,p3,p6;
//错误:
//[p1,p3,p6]=products;
// 0 1 2 0 1 2
//正确:
var [p1, ,p3, , ,p6]=products;
// 0 1 2 3 4 5 0 2 5
//2件事:
// 1. 先声明三个变量
// 2. 再从数组解构
console.log(p1);
console.log(p3);
console.log(p6);
</script>
</body>
</html>
运行结果:
{pname: "小米", price: 3488}
{pname: "小米", price: 2488}
{pname: "华为", price: 5488}
{pname: "苹果", price: 8488}
1
1
{pname: "小米", price: 2488}
{pname: "苹果", price: 8488}
{pname: "OPPO", price: 1488}
- 对象解构:从一个大的对象中仅提取出个别属性值和方法,单独使用!
(1). 如何:
var 变量1, 变量2;
({属性名1:变量1, 属性名2: 变量2, ...}=对象)
(2). 强调: 如果变量已经存在,只需要解构,必须在外围用一个()包裹起来!
(3). 说明: =左边不是一个真正的对象,只是装扮成和=右边对象相同结构。以此来骗取=右边对象的信任,从而顺利拿到=右边对象中同名属性的属性值,保存在=左边的变量里。
(4). 结果: 变量1=对象.属性1
变量2=对象.属性2
(5). 简写: 也可以同时声明变量并解构
var {属性名1:变量1, 属性名2:变量2}=对象
(6). 问题: 因为通常原对象中的属性和方法起名已经很好用了,解构时使用的变量希望沿用原属性名和方法名,就会出现写两遍属性名和方法名的情况:
var {uname:uname, logout:logout }=对象
(7). 简写: 如果希望沿用原属性名和方法名作为解构后的变量名,不改名,则可以只写一个名字:
var {uname, logout}=对象
一个名字两用:既当做配对的属性名,又当做将来单独使用的变量名
(8). 强调: 内部用到this的函数,不能解构。
原理: 解构后,方法名单独使用,调用时,前边什么都没有。方法中的this->window了!不再指向方法所在的对象。this.属性名立刻返回undefined,出错。
(9). 示例: 使用对象解构提取出对象中个别属性和方法:
<!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>
var user={
uname:"dingding",
nicheng:"东东",
upwd:"123456",
login(){ //昨天作业第二个: login:function(){
console.log(`注册...`)
},
logout(){
console.log(`注销...`)
//通常带this的函数不能解构
//console.log(`注销${this.nicheng}成功...`)
}
}
//只想用uname属性值和logout方法
// 属性名:变量,属性名:变量
//var {uname:un, logout:lo}=user;
// console.log(un);
// lo();
//一般原对象中属性和方法名命名已经很好了,没必要改名
// 属性名:变量 , 属性名:变量
//var {uname:uname, logout:logout}=user;
//简写: 当就打算使用原属性名和方法名作为变量名——不改名,其实只写一个名字即可
// var uname,logout;
// ({ uname, logout }=user);
var {uname,logout}=user;
//一个名字两用: 既当做配对的属性名,又当做将来单独使用的变量名!
console.log(uname);
logout();
</script>
</body>
</html>
运行结果:
dingding
注销...
- 参数解构
(1). 问题: 今后如果一个函数多个参数不确定有没有值,但是又要求实参值必须传给指定的形参变量!
(2). 错误的解决: 单靠参数默认值,无法解决如此复杂的问题!
(3). 正确的解决: 参数解构
(4). 如何: 2步:
a. 定义函数时,就要把形参列表装扮为对象的结构
function 函数名({
// 配对 : 接收实参值
属性名1: 形参1="默认值1",
属性名2: 形参2="默认值2",
... : ...
}){
函数体
}
b. 调用函数时,实参值列表,也应该放在一个相同结构的对象中传入
函数名({
属性名: 实参值,
... : ...
})
说明1: 实参值对象结构,不一定包含形参对象结构中所有属性,可以任选任意一个属性传值!
说明2: 实参值对象结构中的:前的属性名不能随意修改,因为必须和定义函数时形参列表对象中的:前的属性名配对,才能成功传入实参值。
(5). 结果: 调用时:
a. 相同属性名对应的实参值,会自动传给定义函数时相同属性名对应的形参变量!
b. 没有提供对应属性名的实参值,则相同属性名的形参变量就收到undefined,从而启用参数默认值。
(6). 简写: 定义函数时,通常形参属性名和形参变量名都是相同相同的。所以其实,定义函数时,可以只写一个形参名。一个名字两用:既当做配对的属性名,又当做形参变量名.
function 函数名({
属性名也是形参名="默认值1",
... =...
}){
函数体
}
(7). 示例: 使用参数解构实现点套餐的函数,可以满足各种点餐换餐需求
<!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>
// function order({
//属性名 : 形参
// 配对 接实参值
// zhushi:zhushi="香辣鸡腿堡",
// xiaochi:xiaochi="薯条",
// yinliao:yinliao="可乐"
// }){
//一个名字两用: 既当做配对的属性名,又当做形参变量名!
function order({zhushi="香辣鸡腿堡",xiaochi="薯条",yinliao="可乐"}){
console.log(`您点的套餐是:
主食:${zhushi},
小吃:${xiaochi},
饮料:${yinliao}
`)
}
//第一个人着急赶火车,希望不用更换任何菜品,直接点套餐默认菜品
order({});
//第二个人不着急,每个菜品都想换:
order({
zhushi:"奥尔良烤腿堡",
xiaochi:"菠萝派",
yinliao:"咖啡"
});
//第三个人只想换饮料,其余两个保持不变
order({
yinliao:"豆浆"
})
//第四个人只想换小吃,其余两个保持不变
order({
xiaochi:"土豆泥"
})
//第五个人想换主食和饮料
order({
zhushi:"巨无霸",
yinliao:"奶茶"
})
</script>
</body>
</html>
七. class:
1. 问题: 旧js中,定义一种类型,需要定义构造函数和原型对象。但是,构造函数和原型对象是分开写的!不像一家人!不符合封装的要求!
2. 解决: 今后只要定义了一种类型都要用class{}包裹构造函数和原型对象方法
3. 如何: 3句话:
(1). 用class{}包裹构造函数和原型对象方法
(2). 将构造函数名放在class之后,作为整个类型的名字。今后所有构造函数名统一改名为"constructor"
(3). 所有原型对象方法,无需再加"类型.prototype"前缀和"=function"。今后只要直接放在class内的函数,默认就是原型对象中的共有方法!
说明: class内构造函数和方法之间,不用逗号和分号分隔!
4. 结果: 除了写代码时,写法简化之外,底层的原理与旧js完全相同!——新瓶装旧酒
5. 示例: 使用class,简化创建学生类型
<!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>
class Student{
constructor(sname,sage){
this.sname=sname;
this.sage=sage;
}
//默认就是放在原型对象中的
intr(){
console.log(`I'm ${this.sname},I'm ${this.sage}`)
}
}
var lilei=new Student("Li Lei",11);
console.log(lilei);
lilei.intr();
</script>
</body>
</html>
运行结果:
Student {sname: "Li Lei", sage: 11}
sage: 11
sname: "Li Lei"
__proto__:
constructor: class Student
intr: ƒ intr()
__proto__: Object
I'm Li Lei,I'm 11
6. 共有属性值:
(1). 错误的做法1: 在构造函数中写死一个属性值
原因: 凡是在构造函数中用this.xxx方式添加的属性,都会成为子对象的自有属性
(2). 错误的做法2: 在class中直接写死一个属性值
原因: 直接放在class中写死的属性值,不会保存在原型对象中成为共有属性,依然会添加到子对象中成为子对象的自有属性
但是,直接写在class中的方法,却是保存在原型对象中的共有方法。
(3). 正确的方法: 依然使用旧js中: 类型名.prototype.共有属性=值
(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>
class Student{
//错误2:
//className="初一2班"
//直接放在class内的属性值,默认不会放在原型对象中,而是成为子对象的自有属性
//今后框架也不能这么写!
constructor(sname,sage){
//错误1:
//this.className="初一2班";//自有属性
//凡是在构造函数中用this.xxx方式添加的属性,都会成为子对象的自有属性
this.sname=sname;
this.sage=sage;
}
//默认就是放在原型对象中的
intr(){
console.log(`I'm ${this.sname},I'm ${this.sage}`)
}
}
//旧js中:
Student.prototype.className="初一2班"
var lilei=new Student("Li Lei",11);
var hmm=new Student("Han Meimei",12);
console.log(lilei);
console.log(hmm);
</script>
</body>
</html>
运行结果:
Student {sname: "Li Lei", sage: 11}
sage: 11
sname: "Li Lei"
__proto__:
className: "初一2班"
constructor: class Student
intr: ƒ intr()
__proto__: Object
Student {sname: "Han Meimei", sage: 12}
sage: 12
sname: "Han Meimei"
__proto__:
className: "初一2班"
constructor: class Student
intr: ƒ intr()
__proto__: Object
7. 两种类型间的继承:
(1). 问题: 两种class之间可能包含部分相同的属性结构和方法定义。如果在两个class中重复定义相同的属性结构和方法,非常不便于今后的维护。
(2). 示例: 使用两种class,描述飞机大战游戏中的敌机和降落伞类型
<!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>
class Plane{
constructor(x,y,score){
this.x=x;
this.y=y;
this.score=score;
}
fly(){
console.log(`飞到x:${this.x},y:${this.y}位置`)
}
getScore(){
console.log(`击落一架敌机得${this.score}分`)
}
}
var p1=new Plane(50,100,5);
console.log(p1);
p1.fly();
p1.getScore();
class San{
constructor(x,y,award){
this.x=x;
this.y=y;
this.award=award;
}
fly(){
console.log(`飞到x:${this.x},y:${this.y}位置`)
}
getAward(){
console.log(`打掉一个降落伞得${this.award}奖励`)
}
}
var s1=new San(100,20,"1 life");
console.log(s1);
s1.fly();
s1.getAward();
</script>
</body>
</html>
运行结果:
Plane {x: 50, y: 100, score: 5}
score: 5
x: 50
y: 100
__proto__:
constructor: class Plane
fly: ƒ fly()
getScore: ƒ getScore()
__proto__: Object
飞到x:50,y:100位置
击落一架敌机得5分
San {x: 100, y: 20, award: "1 life"}
award: "1 life"
x: 100
y: 20
__proto__:
constructor: class San
fly: ƒ fly()
getAward: ƒ getAward()
__proto__: Object
飞到x:100,y:20位置
打掉一个降落伞得1 life奖励
(3). 解决: 再额外定义一个父类型class,集中存储相同部分的属性结构和方法定义。再让两种类型继承父类型class。
(4). 如何: 2步:
a. 先定义父类型class:
1). 父类型class的构造函数中集中定义相同部分的属性结构
2). 父类型原型对象中集中定义相同的方法定义。
b. 让子类型class,继承父类型class: 2步:
1). 让子类型class用extends关键字继承父类型class:
class 子类型 extends 父类型{
2). 在子类型构造函数中用super关键字借用父类型构造函数与子类型构造函数联合,共同创造出子对象中所有属性。
constructor( ... ... ){
super(...) //super自动调整this指向正在创建的新对象,无需自己手动call()
//super必须是子类型构造函数中第一句话!否则报错!
//因为万一出现重复的属性,程序中永远只允许子类型的属性覆盖父类型的属性。绝不允许父类型的属性覆盖子类型!
this.其它属性=xxx;
}
}
(5). 结果: 将来用new 子类型()创建出的子对象,也可以拥有父类型class规定的自有属性,也可以使用父类型class中的共有方法!
(6). 示例: 使用extends优化以上飞机大战程序的class结构
<!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>
class Enemy{
constructor(x,y){
this.x=x;
this.y=y;
}
fly(){
console.log(`飞到x:${this.x},y:${this.y}位置`)
}
}
class Plane extends Enemy{
constructor(x,y,score){
super(x,y);
this.score=score;
//super(x,y)//报错!
}
getScore(){
console.log(`击落一架敌机得${this.score}分`)
}
}
var p1=new Plane(50,100,5);
console.log(p1);
p1.fly();
p1.getScore();
class San extends Enemy{
constructor(x,y,award){
super(x,y);
this.award=award;
//super(x,y)//报错!
}
getAward(){
console.log(`打掉一个降落伞得${this.award}奖励`)
}
}
var s1=new San(100,20,"1 life");
console.log(s1);
s1.fly();
s1.getAward();
</script>
</body>
</html>
运行结果:
Plane {x: 50, y: 100, score: 5}
score: 5
x: 50
y: 100
__proto__:
constructor: class Plane
getScore: ƒ getScore()
__proto__:
constructor: class Enemy
fly: ƒ fly()
__proto__: Object
飞到x:50,y:100位置
击落一架敌机得5分
San {x: 100, y: 20, award: "1 life"}
award: "1 life"
x: 100
y: 20
__proto__:
constructor: class San
getAward: ƒ getAward()
__proto__:
constructor: class Enemy
fly: ƒ fly()
__proto__: Object
飞到x:100,y:20位置
打掉一个降落伞得1 life奖励
八. Promise
1:准备
1.1. 区别实例对象与函数对象
- 实例对象: new 函数产生的对象, 称为实例对象, 简称为对象
- 函数对象: 将函数作为对象使用时, 简称为函数对象
1.2. 二种类型的回调函数
1.2.1. 同步回调
- 理解: 立即执行, 完全执行完了才结束, 不会放入回调队列中
- 例子: 数组遍历相关的回调函数 / Promise 的 excutor 函数
1.2.2. 异步回调
- 理解: 不会立即执行, 会放入回调队列中将来执行
- 例子: 定时器回调 / ajax 回调 / Promise 的成功|失败的回调
1.3. JS 的 error 处理
1.3.1. 错误的类型
- Error: 所有错误的父类型
- ReferenceError: 引用的变量不存在
- TypeError: 数据类型不正确的错误
- RangeError: 数据值不在其所允许的范围内
- SyntaxError: 语法错误
1.3.2. 错误处理
- 捕获错误: try … catch
- 抛出错误: throw error
1.3.3. error 对象的结构
- message 属性: 错误相关信息
- stack 属性: 函数调用栈记录信息
2:promise 的理解和使用
2.1. Promise 是什么?
2.1.1. 理解
- 抽象表达: Promise 是 JS 中进行异步编程的新的解决方案(旧的是谁?)
- 具体表达:
(1) 从语法上来说: Promise 是一个构造函数
(2) 从功能上来说: promise 对象用来封装一个异步操作并可以获取其结果
2.1.2. promise 的状态改变
- pending 变为 resolved
- pending 变为 rejected
说明: 只有这 2 种, 且一个 promise 对象只能改变一次 无论变为成功还是失败, 都会有一个结果数据 成功的结果数据一般称为 vlaue, 失败的结果数据一般称为 reason
2.1.3. promise 的基本流程
2.1.4. promise 的基本使用
<script>
// 1. 创建一个新的promise对象
const p = new Promise((resolve, reject) => {// 执行器函数 同步回调
console.log('执行 excutor')
// 2. 执行异步操作任务
setTimeout(() => {
const time = Date.now() // 如果当前时间是偶数就代表成功, 否则代表失败
// 3.1. 如果成功了, 调用resolve(value)
if (time % 2 == 0) {
resolve('成功的数据, time=' + time)
} else {
// 3.2. 如果失败了, 调用reject(reason)
reject('失败的数据, time=' + time)
}
}, 1000);
})
console.log('new Promise()之后')
// setTimeout(() => {
p.then(
value => { // 接收得到成功的value数据 onResolved
console.log('成功的回调', value)
},
reason => {// 接收得到失败的reason数据 onRejected
console.log('失败的回调', reason)
}
// )
// }, 2000);
</script>
2.2. 为什么要用 Promise?
2.2.1. 指定回调函数的方式更加灵活
- 旧的: 必须在启动异步任务前指定
- promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函 数(甚至可以在异步任务结束后指定/多个)
2.2.2. 支持链式调用, 可以解决回调地狱问题
- 什么是回调地狱?
回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调执行的条件 - 回调地狱的缺点?
不便于阅读 不便于异常处理 - 解决方案?
promise 链式调用 - 终极解决方案?
async/await
2.3. 如何使用 Promise?
2.3.1. API
-
Promise构造函数: Promise (excutor) {}
excutor函数: 同步执行 (resolve, reject) => {}
resolve函数: 内部定义成功时我们调用的函数 value => {}
reject函数: 内部定义失败时我们调用的函数 reason => {}
说明: excutor会在Promise内部立即同步回调,异步操作在执行器中执行 -
Promise.prototype.then方法: (onResolved, onRejected) => {}
onResolved函数: 成功的回调函数 (value) => {}
onRejected函数: 失败的回调函数 (reason) => {}
说明: 指定用于得到成功value的成功回调和用于得到失败reason的失败回调
返回一个新的promise对象 -
Promise.prototype.catch方法: (onRejected) => {}
onRejected函数: 失败的回调函数 (reason) => {}
说明: then()的语法糖, 相当于: then(undefined, onRejected) -
Promise.resolve方法: (value) => {}
value: 成功的数据或promise对象
说明: 返回一个成功/失败的promise对象 -
Promise.reject方法: (reason) => {}
reason: 失败的原因
说明: 返回一个失败的promise对象 -
Promise.all方法: (promises) => {}
promises: 包含n个promise的数组
说明: 返回一个新的promise, 只有所有的promise都成功才成功, 只要有一个失败了就直接失败 -
Promise.race方法: (promises) => {}
promises: 包含n个promise的数组
说明: 返回一个新的promise, 第一个完成的promise的结果状态就是最终的结果状态
2.3.2. promise 的几个关键问题
-
如何改变promise的状态?
(1)resolve(value): 如果当前是pendding就会变为resolved
(2)reject(reason): 如果当前是pendding就会变为rejected
(3)抛出异常: 如果当前是pendding就会变为rejected -
一个promise指定多个成功/失败回调函数, 都会调用吗?
当promise改变为对应状态时都会调用 -
改变promise状态和指定回调函数谁先谁后?
(1)都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
(2)如何先改状态再指定回调?
①在执行器中直接调用resolve()/reject()
②延迟更长时间才调用then()
(3)什么时候才能得到数据?
①如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
②如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据
// 常规: 先指定回调函数, 后改变的状态
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1) // 后改变的状态(同时指定数据), 异步执行回调函数
}, 1000);
}).then(// 先指定回调函数, 保存当前指定的回调函数
value => {},
reason => {console.log('reason', reason)}
)
// 如何先改状态, 后指定回调函数
new Promise((resolve, reject) => {
resolve(1) // 先改变的状态(同时指定数据)
}).then(// 后指定回调函数, 异步执行回调函数
value => {console.log('value2', value)},
reason => {console.log('reason2', reason)}
)
console.log('-------')
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1) // 后改变的状态(同时指定数据), 异步执行回调函数
}, 1000);
})
setTimeout(() => {
p.then(
value => {console.log('value3', value)},
reason => {console.log('reason3', reason)}
)
}, 1100);
-
promise.then()返回的新promise的结果状态由什么决定?
(1)简单表达: 由then()指定的回调函数执行的结果决定
(2)详细表达:
①如果抛出异常, 新promise变为rejected, reason为抛出的异常
②如果返回的是非promise的任意值, 新promise变为resolved, value为返回的值
③如果返回的是另一个新promise, 此promise的结果就会成为新promise的结果 -
promise如何串连多个操作任务?
(1)promise的then()返回一个新的promise, 可以开成then()的链式调用
(2)通过then的链式调用串连多个同步/异步任务
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("执行任务1(异步)")
resolve(1)
}, 1000);
}).then(
value => {
console.log('任务1的结果: ', value)
console.log('执行任务2(同步)')
return 2
}
).then(
value => {
console.log('任务2的结果:', value)
return new Promise((resolve, reject) => {
// 启动任务3(异步)
setTimeout(() => {
console.log('执行任务3(异步))')
resolve(3)
}, 1000);
})
}
).then(
value => {
console.log('任务3的结果: ', value)
}
)
结果
执行任务1(异步)
任务1的结果: 1
执行任务2(同步)
任务2的结果: 2
执行任务3(异步))
任务3的结果: 3
-
promise异常传/穿透?
(1)当使用promise的then链式调用时, 可以在最后指定失败的回调,
(2)前面任何操作出了异常, 都会传到最后失败的回调中处理 -
中断promise链?
(1)当使用promise的then链式调用时, 在中间中断, 不再调用后面的回调函数
(2)办法: 在回调函数中返回一个pendding状态的promise对象
new Promise((resolve, reject) => {
// resolve(1)
reject(1)
}).then(
value => {
console.log('onResolved1()', value)
return 2
},
// reason => {throw reason}
).then(
value => {
console.log('onResolved2()', value)
return 3
},
reason => {throw reason}
).then(
value => {
console.log('onResolved3()', value)
},
reason => Promise.reject(reason)
).catch(reason => {
console.log('onReejected1()', reason)
// throw reason
// return Promise.reject(reason)
return new Promise(() => {}) // 返回一个pending的promise 中断promise链
}).then(
value => {
console.log('onResolved3()', value)
},
reason => {
console.log('onReejected2()', reason)
}
)
3:async 与 await
-
async 函数
函数的返回值为promise对象
promise对象的结果由async函数执行的返回值决定 -
await 表达式
await右侧的表达式一般为promise对象, 但也可以是其它的值
如果表达式是promise对象, await返回的是promise成功的值
如果表达式是其它值, 直接将此值作为await的返回值 -
注意:
await必须写在async函数中, 但async函数中可以没有await
如果await的promise失败了, 就会抛出异常, 需要通过try…catch来捕获处理
// async函数的返回值是一个promise对象
// async函数返回的promise的结果由函数执行的结果决定
async function fn1() {
return 1
// throw 2
// return Promise.reject(3)
// return Promise.resolve(3)
/* return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(4)
}, 1000);
}) */
}
const result = fn1()
// console.log(result)
result.then(
value => {
console.log('onResolved()', value)
},
reason => {
console.log('onRejected()', reason)
}
)
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve(5)
reject(6)
}, 1000);
})
}
function fn4() {
return 6
}
async function fn3() {
try {
// const value = await fn2() // await右侧表达为promise, 得到的结果就是promise成功的value
const value = await fn1()
console.log('value', value)
} catch (error) {
console.log('得到失败的结果', error)
}
// const value = await fn4() // await右侧表达不是promise, 得到的结果就是它本身
// console.log('value', value)
}
fn3()
4: JS 异步之宏队列与微队列
- 原理图
- 说明
1 JS 中用来存储待执行回调函数的队列包含2个不同特定的列队
2 宏列队: 用来保存待执行的宏任务(回调), 比如: 定时器回调/DOM 事件回调 /ajax 回调
3 微 列 队 : 用 来 保 存 待 执 行 的 微 任 务 ( 回 调 ), 比 如 : promise 的 回 调 /MutationObserver 的回调
4 JS 执行时会区别这 2 个队列
(1) JS 引擎首先必须先执行所有的初始化同步任务代码
(2) 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出 来执行