JavaScript

重要

著名程序员 Jeff Atwood 甚至提出了一条 [“Atwood 定”](http://www.codinghorror.com/blog/2007/07/the-principle-of-least-power.html):"所有可以用 JavaScript 编写的程序,最终都会出现 JavaScript 的版本。(Any application that can be  written in JavaScript will eventually be written in JavaScript.)""

1.学习 JavaScript,很大一部分时间是用来搞清楚哪些地方有陷阱。Douglas Crockford 写过一本有名的书,名字就叫[《JavaScript: The Good Parts》](http://javascript.crockford.com/),言下之意就是这门语言不好的地方很多,必须写一本书才能讲清楚。
2.尽管如此,目前看来,JavaScript 的地位还是无法动摇。加之,语言标准的快速进化,使得 JavaScript 功能日益增强,而语法缺陷和怪异之处得到了弥补。所以,JavaScript 还是值得学习,况且它的入门真的不难。
3.作为项目负责人,你不难招聘到数量众多的 JavaScript 程序员;作为开发者,你也不难找到一份 JavaScript 的工作。
4.JavaScript 这个名字的原意是“很像Java的脚本语言”
if、for、Switch、try catch等等"语句"里方括号{}内声明的变量不和"函数"一样,就是普通的,会进行提升,提升到{}外部的,而"函数"的只会在{}内作用,也就是函数作用域

内嵌和外链不能共用一个script标签,这样内嵌式的不会执行只会执行外链的

 <script src="hello.js">
  	alert('hello world4'); // 不会执行,不能这么做
 </script> 
1.数字后的零是没有意义的浏览器不会显示如:3.00000会直接显示为3
2.字符加单引号双引号无所谓,单引号 和 双引号 在 `JS` 里面没有区别,但是不可混用。
3.事件是指你在软件设计中如鼠标单击或者鼠标划过你所希望你的程序会出现什么事件(如弹窗、说明等等事件)
4.运算符之间最好留有空格
5.可以给一个未定义的变量赋值但不能直接访问一个未定义的变量**(console.log(a)  报错为a未定义,因为访问一个值的时候是会默认调用valueOf()方法的。自己)
6.JSON字符串:key和value都由双引号引起来,最外面用单引号括住
7.行内式`js`代码可以访问内部式和外部式`js`代码,内部式和外部式代码互相访问看放置的顺序
8.全局作用域中声明的变量和函数,会作为window对象的属性和方法保存
9.所有对象类型都可以动态添加属性和方法,简单类型不可以动态添加属性和方法
10.字符串一旦定义了就不能修改其中的值,但可以整个重新赋值

其他

字符编码

tyr...catch

'try ... catch ... finally'
1.try语句包含了了由一个或多个语句组成的try块,和至少一个catch或者一个finally块,或者两者都有,其形式有如下三种:
    try ... catch
    try ... finally
    try ... catch ... finally
2.那么finally又是何方神圣呢?在finally语句块中的代码一定会执行,也就是说:
    如果try语句块中的代码全部正常执行,finally语句块中的代码会执行;
    如果try语句块中的代码在执行的过程中出现错误,转而执行catch语句块中的代码,finally语句块中的代码最终也会被执行。
3. try…catch…finally语句中的return值以finally的为准
    (() => {
    try {
        console.log(123);
        return 123;
    } catch {
        console.log(456);
        return 456;
    } finally {
        console.log(789);
        return 'acc';
    }
    })()
    // 123
    // 789
    // 返回值 'acc'

'catch 命令的参数省略‘
JavaScript 语言的try...catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。
    try {
      // ...
    } catch (err) {
      // 处理错误
    }
上面代码中,catch命令后面带有参数err。很多时候,catch代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019 做出了改变,允许catch语句省略参数。
    try {
      // ...
    } catch {
      // ...
    }

小括号在JS的用途

小括号()在js中用途:
1.提高优先级 2 + (3 > 4)
2.把代码作为一个整体返回 
console.log(1,2,3);//打印1 2 3
console.log( (1,2,3) );//作为一个整体返回,只返回3(这是逗号操作符,当顺序点用,结合顺序是从左至右,用来顺序求值,完毕之后整个表达式的值是最后一个表达式的值)
3.fnName();//函数调用(执行)

valueOf()

valueOf()函数返回指定对象的原始值。

JavaScript的许多内置对象都重写了该函数,以实现更适合自身的功能需要。因此,不同类型对象的valueOf()方法的返回值和返回值类型均可能不同。

简单类型也是先用valueOf()跟下面的一样(自己)

复杂数据类型转成number数据类型:
([]).valueOf().toString() == '' ; ([]).valueOf().toString() = '[object Object]'
console.log([3] + 3) //33   console.log({} + 3) //[object Object]3
1.先使用 valueOf() 方法获取原始值,如果原始值不是number数据类型,则使用 toString() 方法转成string;
需要注意的:空数组的toString()方法会得到空字符串,空字符串的toString()方法会得到空字符串,而空对象的toString()方法会得到字符串[object Object]



知识借鉴:
    https://www.jianshu.com/p/e4ac366ac2fd
    https://bbs.csdn.net/topics/393726225
	valueOf、浅拷贝深拷贝问题:
        https://blog.csdn.net/celi_echo/article/details/100038366
        https://blog.csdn.net/magic_xiang/article/details/83686224    
        https://blog.csdn.net/hb_zhouyj/article/details/78343021       

eval()

eval 可以将一段字符串当作js代码来执行
  var str = '1+1'
  var num = eval(str)
  console.log(num)//2
  一般不用
  
  
  let json = "{'a':3,'b':4}"
    let j = eval('('+json+')')
    console.log(j)

JSON.parse()和eval()区别

JSON.parse()和eval()区别
JSON.parse()只会将标准的Json字符串(key和value都由双引号引起来,最外面用单引号括住)转为JSON对象。

eval()在转换字符串的时候是比较松的,即使不是标准的Json字符串也会被转换成Json对象。更重要的是eval()方法会执行要解析的字符串中的代码,这一点是十分危险的。
    比如:
        var temp1 = 1;
        var temp2 = '++temp1';
        temp2用JSON.parse()方法解析会报错,而用eval()方法却会得到2.

记住一点:程序里面使用eval()方法解析是很危险的代码,尽量使用JSON.parse()。

JavaScipt

JS基本知识

JS概述

js作用:
    数据提交到服务器之前验证数据
    给html网页增加动态功能
    响应用户操作
    加强用户体验

1. JavaScript是一种直译式脚本语言,是一动态类型、弱类型、基于原型的语言
    直译式:直接编译并执行,代码不进行预编译。
    脚本:凡是不能独立执行需要依赖其他程序的,通常都叫做脚本。
    动态类型:声明一个变量,能够随时改变它的类型
    静态类型:声明一个变量,不能改变它的类型
    弱类型:允许变量类型的隐式转换,允许强制类型转换(编译时检查很弱)
    强类型:变量对象在编译时确定类型,不允许类型的隐式转换(编译时检查严格)
    基于原型:原型的主要作用是继承,对象之间的继承,主要是靠原型链接
2. Javascript是一门面向对象的,跨平台的脚本语言 
3. HTML文件必须在浏览器里执行,JS文件必须嵌入到HTML文件里才能执行。HTML标记语言
4. JavaScript脚本语言:依附于某个程序来运行,为某个程序提供服务的语言。JavaScript编程语言。 特性:跨平台

JavaScript特点
    解释性脚本语言
    运行在浏览器(浏览器内核带有js解释器,Chrome v8引擎)
    弱类型语言(松散型)
    事件驱动(动态)
    跨平台

JavaScript运用
    网页前端开发(JavaScript的老本行)
    移动开发webapp混合式应用
    网站后端开发(Node.js可以让js在后端运行)
    插件开发(由于js跨平台这一特性,很多插件使用js进行开发,因为一次开发可以保证平台使用)
    桌面开发(主要是指chrome浏览器能把js写的程序打包成左面应用)
    游戏开发  等等...

JavaScript在网页端的作用
    数据提交到服务器之前验证数据
    给html网页增加动态功能
    响应用户操作
    加强用户体验

JS的组成

1. ECMASCRIPT:定义了JavaScript的语法规则,描述了语言的基本语法和数据类型。JavaScript的语法
2. BOM(Browser Object Model):浏览器对象模型。有一套完整的操作浏览器的属性和方法,有一套成熟的可以操作浏览器的API,通过BOM可以操作浏览器,比如:弹出框、浏览器跳转、获取分辨率等
3. DOM (Document Object Model): 文档对象模型。有一套完整的操作页面元素的属性和方法,有一套成熟的可以操作页面元素的 API,通过 DOM 可以操作页面中的元素。比如: 增加个 div,减少个 div,给 div 换个位置等
   
总结:
1. JS 就是通过"固定的语法"去操作 浏览器 和 页面元素 来实现网页上的各种效果
2. JavaScript由三大部分组成:核心部分(ECMA-262)、DOM、BOM

JS的引入

JS也有多种方式书写分为行内式、内嵌式、外链式

1.行内式:写在标签上的js代码需要依靠事件(行为)来触发
    写在 a 标签的 href 属性上
       <a href="javascript:alert('我是一个弹出层');">点击一下试试</a>
	   因为a标签自身就默认有点击事件,所以我们会加一个javascript:去覆盖a标签自带的点击默认事件;

       如果不加javascript则a会将其当成跳转的链接(即a标签的默认事件被执行),如:
       <a href="alert('我是一个弹出层');">点击一下试试</a>
   
写在其他元素上
    <div onclick="alert('我是一个弹出层')">点一下试试看</div>
    因为是非a标签自身没有默认的点击事件,所以写代码就需要先给标签加一个可以点击触发执行的事件onclick
	注:onclick 是一个事件(点击事件),当点击元素的时候执行后面的 js 代码

2.内嵌式: script里的程序整个页面都可以使用
    内嵌式的 js 代码会在页面打开的时候直接触发
    在 html 页面书写一个 script 标签,标签内部书写 js 代码
    <script type="text/javascript">
    	alert('我是一个弹出层')
    </script>

注:script 标签可以放在 head 里面也可以放在 body 里面(是可以放在任何位置的,但最好放在body结束位置后)

3.外链式: script里的程序整个页面都可以使用
    外链式js代码只要引入了html页面,就会在页面打开的时候直接触发
    步骤:新建一个.js后缀的文件,在文件内书写js代码,把写好的js文件引入html页面
        alert('我是一个弹出层') /* index.js 文件 */
        <script src="index.js"></script> /* html文件 */

    /* 一个页面可以引入多个 js 文件 */
    <script src="index1.js"></script>
    <script src="index2.js"></script>
    <script src="index3.js"></script>

注:前面的先执行后面的后执行,就是按照从上到下的顺序执行并不会被覆盖
link 标签是为了引入 css 文件使用的,link 使用 href 属性
script 标签是为了引入 js 文件使用的,script 使用 src 属性,和你写在什么位置没有关系

JS中的注释

"学习一个语言,先学习一个语言的注释,因为注释是给我们自己看的,也是给开发人员看的。写好一个注释,有利于我们以后阅读代码"

1. 单行注释:一般就是用来描述下面一行代码的作用.可以直接写两个 `/` ,也可以按 `ctrl + /`    
    // 我是一个单行注释
    
2. 多行注释:一般用来写一大段话,或者注释一段代码,可以直接写 `/**/` 然后在两个星号中间写注释,各个编辑器的快捷键不一样,vscode编辑器的是 `ctrl + shift + a`
    /**
     * 我是一个多行注释
     * 注释的代码不会执行
     * 用于插件或方法开头位置
     * 说明参数,功能等说明
	 * alert('我是一个弹出层')
     */

JS运行和编译

js预解析(编译)

程序执行顺序:从上到下,从左到右
`执行之前先预解析后再执行,譬如:运行到函数时函数内部有声明变量等,这个时候函数内部先预解析后再执行`

1.预解析:执行之前进行预解析
    var、function关键字提前到当前作用域的顶部并自动赋予默认值,变量默认值为undefined,函数默认值为函数体代码块,当函数与变量重名时,保留函数(如果没有var,function关键词则需要等到运行时发现后才会进行提升--自己)。

alert(a); // function a(){alert(3);}
var a = 1;
alert(a); // 1
function a(){alert(2);}
alert(a); // 1
var a = 3;
alert(a); // 3
function a(){alert(3);}
alert(a); // 3
"解释执行" 见下方代码
    例子1:
    	console.log(map)
        function map(){
          console.log(a)
          a=2
        }
        map()

	打印:
	   ƒ map(){ // console.log(map)打印的信息
          console.log(a)
          a=2
        }
	   Uncaught ReferenceError: a is not defined //报错信息
       
    "解析:首先执行前进行预解析,查找var、function将其进行提升到头部,所以函数map被提升到头部,map函数内的变量a因为是没有var在预解析阶段并没有被提升到函数顶部,而当执行map()的直行道console.log(a)时需要打印a就会通过作用域向外寻找,因为外部也没有,而又因为规定(可以给一个未定义的变量赋值但不能直接访问一个未定义的变量,否则报错 ),所以这里打印变量a会直接报错"

    例子2:
        var a = 4
        function map(){
          console.log(a) // 4
          a=2 
        }
        map()
        console.log(a) // 2

"注意:可以给一个未定义的变量赋值但不能直接访问一个未定义的变量console.log(a) // 报错为a未定"

例子

var a=1;
function fn(a){
  alert(a); // undefined
  a=2;
}
fn();
alert(a); // 1


var a=1;
function fn(a){
  alert(a); // 1 
  a=2;
}
fn(a);
alert(a); // 1


console.log(num); // undefined
var num = 24;
console.log(num); // 24
func(100 , 200); 
function func(num1 , num2) {
var total = num1 + num2;
console.log(total); // 300
}


fn(); // 2
function fn() {console.log(1);}    
fn(); // 2
var fn = 10; // 这里赋值的函数已经成了数值,所以已经不想存在fn这个函数了    
fn(); // 报错
function fn() {console.log(2);}    
fn(); // 不执行


var a = 1;
fn();
function fn(){
a = a + 1;
console.log(a); // 2  3  4  5
return a;
}
fn();
console.log( fn() + 1 );

变量

变量的作用

1.变量是指没有固定的值,可以改变的数;是存储信息的容器
2.JS的变量是松散类型的,可以用来保存任何类型的数据
    关键字  变量名   赋值  数据
    var    userName  =   'xiaocuo';
    左值:在等好左侧,是变量名
    右值:在等号右侧,是存进变量中的数据

3.变量的底层原理:从本质上看,变量代表了一段可操作的内存,也可认为变量是内存的符号化表示。当我们使用关键字var、function、let、const ...等等,声明一个变量的时候,解析器根据变量的数据类型分配一定大小的内存空间。程序就可以通过变量名来访问对应的内存了。

4.变量指的是在程序中保存数据的一个容器变量是计算机内存中存储数据的标识符,根据变量名称可以获取到内存中存储的数据,通俗的说就是"我们向内存中存储了一个数据,然后要给这个数据起一个名字,为了是我们以后再次找到他"

变量的命名规则和命名规范

规则: 必须遵守的,不遵守就是错
1. 变量名首字符必须为字母(a-z A-Z),下划线( _ ),或者美元符号( $ )开始,余下的字符可以是下划线、美元符号或任何字母或数字字符
2.严格区分大小写
3.不能由数字开头
4.不能是保留字或者关键字
5.不能含有空格

规范: 建议遵守的(开发者默认),不遵守不会报错
1. 变量名尽量有意义(语义化)
2. 遵循驼峰命名规则,由多个单词组成的时候,从第二个单词开始首字母大写
3. 不要使用中文

变量声明方式

`这里只针对var的声明变量的方式`

1.定义一个变量,不进行赋值
    var num; 
    num = 100;  

2.定义一个变量的同时给其赋值
    var num2 = 200;
    使用方式:
    定义多个变量:var n1, n2, n3...
    定义多个变量并赋值:var n1 = 1, n2 = 2,....
    定义多个变量有的赋值有的不赋值:var n1 = 1, n2, n3 = 4, ...

注意:
1. 一个变量名只能存储一个值
2. 当再次给一个变量赋值的时候,前面一次的值就没有了
3. 变量名称区分大小写(JS严格区分大小写)

"目前声明变量的方式一共有6种"
    ES5版本为:var命令和function命令。
    ES6版本为:let和const命令、import命令和class命令
    加起来一共是6种声明变量的方式

常用输出/调试方法

alert() 浏览器弹窗,弹出的内容就是括号中的内容
document.write() 直接向文档写入字符串、HTML(用i"字符串形式"书写HTML)或JavaScript代码(语句直接打印结果)
console.log() 在控制台打印相关信息(语句直接打印结果)
prompt()  弹出一个输入框,用户可以输入内容,而且输入的内容可以用一个变量存起来

详解:
1. alert('我是一个弹出层') 括号里面只要不写纯数字或者语句,就要用引号包裹起来(单引号双引号无所谓)因为除了数字就是字符
如:<script type="text/JavaScript">
       document.write(alert('hello world 6'));
       document.write('<h2>一个标题</h2>');
       document.write('hello world5');
	   alert(2) /* 纯数字 */
	   alert(1+1) /* 语句 */
    </script>
 
2. prompt(str1,str2)  弹出可输入的对话框
    str1: 要显示在消息对话框中的文本(提示文字)
    str2:文本框中的内容(输入的内容)
    返回值:
    1. 点击确定按钮,文本框中的内容将作为函数的返回值
    2. 点击取消按钮,将返回null
    3. 输入的值自动会转化成"字符类型"


注意:调试代码应当从最初的产品代码中删除掉(输出语法就是为了我们在开发的时候检测一下我们计算的数据对不对
比如写了2000行的代码了, 忘了这个变量当初存储的是100还是200了你可以翻回去看看, 也可以直接在控制台打印一下让他显示出来)

常见报错

JS为单线程,同一个时间只能做一件事,做完这一个才能做下一个,所以只要报错了这件事就还没做完,后面的就不会执行所以一定要优先解决报错(单线程特征如果程序报错(即未执行完)那么后面的程序不会接着执行会中断在这里)

1. Uncaught   SyntaxError:  Unexpected        identifier    helloWorld.html:34
    捕获         语法错误  :    意外的        报文( 标识符 )           报错位置

2. Uncaught SyntaxError: Unexpected token xx: 使用了xx关键字

3. Uncaught ReferenceError: hello is not defined   变量.html:31 
   未捕获     引用错误      : 报文(hello 未定义)   报错位置

4. Uncaught RangeError: Maximum call stack size exceeded
     未捕获     范围错误 : 内存溢出
 
5. Uncaught TypeError: Cannot set property 'b' of undefined; 类型错误 : 不能给undefined设置属性b;
 
6. var str1 = un.toString();
   console.log(str1);
   Uncaught TypeError: Cannot read property 'toString' of undefined
     未捕获    类型错误: 报文(undefined不能读取到属性'toString')(即此对象并不存在这个属性)

报错后运行规则

function test() {
  throw new Error('失败了')
} 
test()
setTimeout(()=>{console.log(33)},1000)
// Uncaught Error: 失败了

setTimeout(()=>{console.log(33)},1000)
function test() {
  throw new Error('失败了')
} 
test()
// Uncaught Error: 失败了
// 3

由上可以看出在报错之前执行的代码都会执行完

表达式

表达式定义:
    1.表达式是指能计算出'值'的任何可用程序单元。  ——wiki
    2.表达式是js一种短语,可使js解释器来产生一个'值'。  ——js《权威指南》

    '原始表达式'
        常量/直接量: 3.14/ "test" 等
        关键字: null, this, true, false 等
        变量: i, k, j 等
        复合表达式:原始表达式和原始表达式之间用运算符连接可以组成一个复合表达式。
        例如: 10 * 20、i + k(var i = 1, k = 2) 这些就是复合表达式
        
    '(数组、对象的)初始化表达式'
        [1,2]  or  new Array(1, 2);
        [1, , , 2]  or  new Array(1, undefined, undefined, 2);
        {x: 1, y: 2} or var obj = new Object();

	'函数表达式'
        var fn = function (){};
        (function (){ console.log("hello") })();

	'属性访问表达式'
		obj.x or obj[x] ...
    
    '调用表达式'
		fun()

	'对象创建表达式'
		new Func (1, 2)

	'...等等'

总结:'可使js解释器用来产生一个值'的就是一个表达式;
其他:单个表达式也可称作语句,表达式与表达式的组合是语句,语句和语句的组合是程序

知识借鉴: https://blog.51cto.com/u_14987/6823447

数据类型

'基本类型'
基本数据类型指的是简单的数据段,引用数据类型指的是有多个值构成的对象
基本类型:Number、Boolean、String、Null和underfined
基本数据类型是保存在栈内存中的简单数据段,是按值访问的,可以直接操作保存在变量中的实际值。

'复杂类型'
引用类型:对象类型,如Object、Array、Function等等,内存里存的是引用(地址)
引用类型是保存在堆内存中的复杂数据段(对象),是操作引用地址的,不能可以直接操作保存在变量中的实际值,只能操作对象在栈内存中的引用地址。

JS里面的数据类型我们分成两个大类: 我们存储在内存中的数据的类型,我们通常分为两大类 "基本数据类型"和"复杂数据类型" 基本数据类型又叫简单数据类型,复杂数据类型又叫引用类型/地址数据类型

1. JS中有6种数据类型:"Number、Boolean、String、Null、Undefined 和 Object(Object, array, function)"。JS中不支持任何创建自定义类型的机制,而所有值最终都将是这6种数据类型。其数据类型的动态性特点足以表示所有数据。
2. JS提供typeof操作符来检测变量的数据类型。
	typeof返回的是字符串有六种可能:"number","boolean","string","undefined","object","function"
3. ES6引入的新的原始类型: Symbol原始数据类型

____________________________________________________

一:JS变量的两种数据类型
1.基本类型
包括:number,boolean,string,null,undefined

2.引用类型
包括:Object,Array,Function

二:两种类型的区别
1.基本类型的访问是按值访问的;引用类型是按引用地址访问的;
2.基本类型可以操作保存在变量中的实际的值;
3.基本类型不能添加属性和方法,添加只会让其不会保存,访问不了;引用类型可以添加属性和方法,也可以删除属性和方法;
4.基本类型的比较是值的比较;引用类型比较的是引用比较;
5.基本类型的变量是存放在栈里的;引用类型是保存在堆和栈中的,引用地址在栈中,引用的对象在堆中;
6.基本类型赋值属于简单赋值,被赋值变量不会影响原赋值变量;引用类型的对象赋值给另一个对象,值改变时会相互影响,除非重新赋值,隔断引用地址;

三:typeof来区分类型
typeof返回有六种值: number、boolean、string、undefined、object(null,array)、function

知识借鉴:https://www.cnblogs.com/Sedfabt-xiaofeng/p/12732305.html
		https://www.cnblogs.com/focusxxxxy/p/6390536.html
		https://www.cnblogs.com/wcs344292264/p/5368861.html

简单类型

Number

一切数字都是数值类型(包括二进制,十进制,十六进制等)
    var num2 = 222;
    var no1 = 333.12300;
    输出时数字后的零不会显示12300后的两个零

number转换: 整个可以转换成数字,那么就给你一个数字,整个不能转换成一个数字, 那么就是 NaN
parseInt、parseFloat
parseInt(变量)
从第一位开始检查,是数字就转换,直到一个不是数字的内容
开头就不是数字,那么直接返回 `NaN`(忽略前面空格)
不认识小数点,只能保留整数

parseFloat(变量)
从第一位开始检查,是数字就转换,一个不是数字的内容
开头就不是数字,那么直接返回 `NaN`(忽略前面空格)
认识一次小数点


parseInt()解析一个字符串,并返回一个整数
console.log( parseInt(1234.45) );//1234
console.log( parseInt('1234.45') );//1234
console.log( parseFloat('1234.45') );//1234.45
console.log( parseInt('123abc') );//123
console.log( parseInt('100px') );//100
console.log( parseInt('a100px') );//NaN

parseFloat()解析一个字符串,并返回一个浮点数
Number.toFixed(n)方法可把Number四舍五入保留n位小数(可以用来将数字保留几位小数,并且数据类型变为字符串)
NaN
NaN (not a number): 一个非数值,是number类型中的一个特殊值
NaN用于表示本来要返回一数值的操作数,结果未返回值得情况如console.log('a'-1)
    NaN有两个特点:(Not a Number)
    1.任何涉及NaN的操作都会返回NaN
    2.NaN与任何值都不相等,包括它本身
    isNaN()判断值是否为NaN,返回布尔值
    1.1错误的计算结果:NaN (not a number) 非数字
    当进行计算,希望返回一个数值,结果并不是我们想要的数字时,返回 NaN

console.log( str + num );// 'abc10'  + 在字符串中,表示 连接 拼接
console.log( 'abc' + '10' );// 'abc10'  拼接
当进行计算,希望返回一个数值,结果并不是我们想要的数字时,返回 NaN
console.log( str * num );//NaN
console.log( str - num );//NaN
console.log( str / num );//NaN
console.log( typeof NaN );//number

var x = str * num;//NaN
console.log( x - 10 );//NaN
console.log( x + 10 );//NaN
console.log( NaN == NaN );//false

NaN的作用:当进行一个计算而计算机又不能返回一个我们想要的数字(就是真正的值,简而言之就是发生错误),但又不能让计算机报错,所以才返回一个NaN的值(因为报错后程序会中断运行,其就是为了不让程序中断运行)


`如何判断值是否为NaN`
工具 isNaN():判断值是否为NaN
如:console.log( isNaN(x) );//true  (判断上方X是否为NaN)


1. isNaN隐式类型转换(调用了Number进行了隐式类型转换)
    Number强制类型转换
    console.log( Number('123') );//123
    console.log( Number(456) );//456
    console.log( Number(true) );//1
    console.log( Number('123a') );//NaN  
    Number(0x123a)//4666

    console.log( isNaN('123') );//false
    console.log( isNaN(456) );//false
    console.log( isNaN(true) );//false
    console.log( isNaN('123a') );//true 是NaN

2. isNaN的使用
    var value = '13520559717';
    if (isNaN(value)) {
        // 不是纯数字
    } else {
        // 是纯数字
    }

3.isNaN和Number.isNaN
    isNaN方法首先转换类型,而Number.isNaN方法不用;
    isNaN不能用来判断是否严格等于NaN,Number.isNaN方法可用

Boolean

 Boolean类型是ECMAScript中使用最多的一种类型,该类型只有两个字面值:true和false。这两个值与数字值不是一回事,因此true不一定等于1,而false也不一定等于0。需要注意的是,Boolean类型的字面值true和false是区分大小写的。也就是说,True和False(以及其他的混合大小写形式)都不是Boolean值,只是标识符。

    虽然Boolean类型的字面值只有两个,但ECMAScript中所有类型的值都有与这两个Boolean值等价的值。
     var file="Hello World!";
     var fileNew =Boolean(file);

    在这个例子中,字符串file被转换成了一个Boolean值,该值被保存在fileNew变量中。可以对任何数据类型的值调用Boolean()函数,而且总会返回一个Boolean值。值域返回的这个值是true或false,取决于要转换的值的数据类型及其实际值。下表给出了各种数据类型及其对应的转换规则。

    根据Boolean转换规则,我们可以得出以下示例的结果:

     var color=new Boolean();
     var color=new Boolean(0);
     var color=new Boolean(null);
     var color=new Boolean("");
     var color=new Boolean(false);
     var color=new Boolean(NaN);
    以上color的值均为false。
    
     var color=new Boolean(1);
     var color=new Boolean(true);
     var color=new Boolean("true");
     var color=new Boolean("false");  //非空字符串转换为true
     var color=new Boolean("Hello World");
    以上color的值均为true。

    由于JS存在自动执行的Boolean转换,因此确切地知道在流控制语句中使用的是什么变量至关重要。错误地使用判断条件,就可能导致彻底改变应用程序的流程。1  

String

被引号包裹的所有内容(可以是单引号也可以是双引号)
    var str1="abc";
    var str2="123";

`变量.toString()`
有一些数据类型不能使用 `toString()` 方法,比如 `undefined` 和 `null`如果你使用了, 那么会直接报错 (自己:因为undefined和null前者不是对象,后者是空对象(就是什么都没有,就连继承也没有))
var boo=true;
console.log(true.toString());//`true`
console.log((123).toString());//`123`


`String(变量)`
所有数据类型都可以
console.log(String(null))//`null`
console.log(String(123));//`123`

{}+[]与console.log({}+[])结果不同?从JavaScript的大括号谈起

知识借鉴:https://www.cnblogs.com/MarcoHan/p/5250876.html

Null

null值表示一个空对象指针,而这也正是使用typeof操作符检测null值会返回“object”的原因,如下面的例子所示:

var car =null;
alert(typeof null); // object(其实这是JavaScript最初实现的一个错误,后来被ECMAScript沿用下来)
如果定义的变量准备在将来用户保存对象,那么最好将该变量初始化为null而不是其他值。这样一来,只要直接检查null值就可以知道相应的变量是否已经保存了一个对象的引用,如下面的例子:

if(car != null){
    //对car执行某些操作
}

实际上,undefined值是派生自null值的,因此ECMA-262规定对他们的相等测试要返回true:

alert(null == undefined)  //true

这里,位于null和undefined之间的相等操作符(==)总是返回true,不过要注意的是,这个操作符出于比较的目的会转换其操作数。

"尽管null和undefined有这样的关系,但它们的用途完全不同,如前所述,无论什么情况下都没有必要把一个变量的值显式地设置为undefined,可视同样的规则对null却不适用。换句话说,只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存null值。这样做不仅可以体现null作为空对象指针的惯例,而且也有助于进一步区分null和undefined。"

_____________________________________________________________________


在程序中如何判断变量是否为null。
1、  var exp = null;
if (!exp && typeof exp != "undefined" && exp != 0)
{
    alert("is null");
}
typeof exp != "undefined" 排除了 undefined; 
exp != 0 排除了数字零和 false。
更简单的正确的方法:

2、 var exp = null;
if (exp === null)
{
    alert("is null");
}


尽管如此,我们在 DOM 应用中,一般只需要用 (!exp) 来判断就可以了,因为 DOM 应用中,可能返回 null,可能返回 undefined,如果具体判断 null 还是 undefined 会使程序过于复杂。

Undefined

Undefined类型只有一个特殊的值——undefined。undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
    1.变量被声明了,但没有赋值时,就等于undefined。
    2.调用函数时,应该提供的参数没有提供,该参数等于undefined。
    3.对象没有赋值的属性,该属性的值为undefined。
    4.函数没有返回值时,默认返回undefined。

例如:
var color;
alert(color == undefined);//true
这个例子只声明了变量color,但未对其初始化。比较这个变量与undefined字面量,结果表明它们是相等的。这个例子与下面的例子是等价的:

var color ; // 变量声明之后默认取得undefined值
alert(color); // "undefined"
alert(age); // 产生错误
"可以给未声明的变量赋值,但不能访问为声明的变量,否则报错"

对未初始化的变量执行typeof操作符会返回undefined值,而对未声明的变量执行typeof操作符同样也会返回undefined值。
var color; // 变量声明之后默认取得undefined值
alert(typeof color); // "undefined"
alert(typeof age); // "undefined"

结果表明,对未初始化(即未赋予初始值,只有默认值)和未声明的变量执行 typeof 操作符都返回了 undefined 值;这个结果有其逻辑上的合理性。因为虽然这两种变量从技术角度看有本质区别,但实际上无论对哪种变量也不可能执行真正的操作。

判断数据类型

为什么要检测数据,都是我们自己写的但是在运算的过程中, 有可能数据类型会发生改变所以我们需要检测数据类型。既然已经把数据分了类型,那么我们就要知道我们存储的数据是一个什么类型的数据,使用 `typeof` 关键字来进行判断。

    var n1 = 100;
    console.log(typeof n1); 或 var n = typeof(n1); console.log(n)



typeof操作符返回一个用来表示数据类型的字符窜 (typeof a != "number")
使用typeof操作符将会返回下列六个字符窜之一:
    "undefined"--值未定义
    "boolean"--值是布尔值
    "string"--值是字符串
    "number"--只是数值
    "object"--值是对象、数组或null
    "function"--值是函数

例子:
    var abc;
    console.log(typeof abc);//undefined

    abc = 123;
    console.log(typeof abc);//number
    console.log(typeof NaN);//number

    abc = 'true';
    console.log(typeof abc);//string

    abc = true;
    console.log(typeof abc);//boolean

    abc = null;
    console.log(typeof abc);//object

    abc = [];
    console.log(typeof abc);//object

    abc = function (){};
    console.log(typeof abc);//function   ECM规定的为了区分普通对象和函数对象而特定这样设定的

    console.log(undefined == null);//true

数据类型转换

1.隐式转换:用一些运算符来隐式转换
2.强制(显示)转换:parseInt()、parseFloat()、Number()、Boolean()、toString()、String()、toFixed()

注:toFixed() 后的数字会转换成字符串类型

强制转换

其他数据类型转成number

Number(变量)
可以把一个变量强制转换成数值类型
可以转换小数,1会保留小数
可以转换布尔值
遇到不可转换的都会返回NaN

将其他的数据类型转换为Number
转换方式一:
    使用Number()函数
    - 字符串 --> 数字
        1.如果是纯数字的字符串,则直接将其转换为数字
        2.如果字符串中有非数字的内容,则转换为NaN
        3.如果字符串是一个空串或者是一个全是空格的字符串,则转换为0
    - 布尔 --> 数字
        true 转成 1
        false 转成 0
    - null --> 数字     0
    - undefined --> 数字 NaN
    - 对象
    	[]、['']、['  ']、[null]、[undefined] 转成 0
    	['3']、[3]、[[3]] 转成 3
    	{}、[{}]、[3, 3] 凡是Object对象的均转成 NaN
    	除了以上这几种,其他都转成NaN

转换方式二:
    - 这种方式专门用来对付字符串
    - parseInt() 把一个字符串转换为一个整数
    - parseFloat() 把一个字符串转换为一个浮点数

其他数据类型转成string

通过 toString() 或 String() 方法即可转换

其他数据类型转成boolean

在 js 中,只有 0、-0、NaN、false、''、null、undefined,这些是 false, 其余都是 true
例子
console.log ( [] == 0 );      // true
console.log ( ! [] == 0 );    // true

console.log ( [] == ! [] );   // true
console.log ( [] == [] );     // false

console.log({} == !{});       // false
console.log({} == {});        // false


{} + [] = 0   
{} - [] = -0   
原因:浏览器把{}当成一个代码模块处理了,直接给忽略了
	
console.log({}+[]) // '[object Object]'
console.log({}-[]) // NaN
原因:console.log({}+[]) // 这种的其里面的被认为是表达式了,而+的两边既没有字符也没有数字,所以先将{}或[]进行转化

隐式转换

基本包装类型

基本包装类型: string  number  boolean
在需要的时候会被临时包装成一个对象————当不需要的时候,如str.len,len不是内置方法,所以str不会临时装换为对象
console.log( str1.length );
console.log( str1.toLowerCase() );

var str4 = 'Hello';//string
var res = str4.toLowerCase();
console.log( res );
当上面的代码执行的时候,系统自动完成以下工作:
    1.找到对应的类,并创建这个类的一个对象(值相同)
    	var _str4 = new String('hello');
    2.调用对象的方法
    	var res = _str4.toLowerCase();
    3.销毁临时对象
    	_str4 = null; 

知识借鉴:https://blog.csdn.net/weixin_44706267/article/details/121331462
引申:不管是基本类型还是复杂类型在访问其值的时候都会默认调用valueOf()函数来获取值,基本类型虽然不是对象,但是在访问值的时候会暂时的变成对象,访问后再进行销毁,就是上面的基本包装类型

运算规则和比较规则

在转换不同的数据类型时,“相等操作”符遵循下列基本规则:
1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值;
2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值;
3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()、toString() 方法,用得到的基本类型值按照前面的规则进行比较;
4. 如果有一个操作数是 NaN,无论另一个操作数是什么,相等操作符都返回 false;
5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象,则相等操作符返回 true;
6. 在比较相等性之前,不能将 null 和 undefined 转成其他值。
7. null 和 undefined 是相等的。

8.其他类型:复杂类型进行运算的时候会自动的进行valueOf()和toString()方法将其值变为字符类型(实际上是变为简单类型)再进行进行运算

9.上面阐述的 1、2、3 三条规则,总结成一句话就是:
如果相等操作符两边的操作数,不包含 null 或者 undefined,且两个操作数不全是对象,在执行相等比较之前,会先调用 Number() 将两个操作数强制转为 Number 类型,然后进行比较

综合4,5,6,8,9即可,同样适用于加减乘除运算,只是加法有一个字符的例外

'自己总结':
1.比较 | 运算规则:
    number:number和number,boolean,string,null,undefined进行'比较或运算'均转换成Number后在进行'比较或运算'
    boolean: boolean和number,boolean,string,null,undefined进行'比较或运算'均转换成Number后在进行'比较或运算'
    string: string和number,boolean,string,null,undefined进行'比较或运算'均转换成Number后在进行'比较或运算'(string和string进行'比较时'直接取码值进行比较)
	null: ...
    undefined: ...

2.运算规则特例: 
	1.对象和对象进行运算时进行valueOf和toString后再运算
	2.number,boolean,string,null,undefined和array,object进行'运算时'前者均转换成Number,后者均使用valueOf和toString进行转换后再进行'运算'
	3.和sting进行“+”运算时简单类型不做任何类型转换操作直接拼接,复杂类型先valueOf和toString后再进行拼接

3.比较运算特例:对象和对象进行'比较时':“==”只比较地址,“===”即比较地址也比较类型

4.方法内运算规则:![] 操作是!(Boolean([]))
		Boolean([]) Boolean({}) // 均是方法内逻辑操作
		String([]) String({})  // 均是方法内逻辑操作

5.一元加号操作符也可以实现与Number()具有的作用
	如:'b'+'a'+ +'a'+'a'=baNaNa
	因为其中第二个 +'a' = NaN,这里的+是一元加号操作符,会自动进行单独Number('a')运算,Number('a') = NaN,剩下的就是字符串相加了,所以是baNaNa

6.其他
在 JS 里面,`+` 由两个含义
字符串拼接: 只要 `+` 任意一边是字符串,就会进行字符串拼接
加法运算:只有 `+` 两边都是数字的时候,才会进行数学运算

知识借鉴: https://www.cnblogs.com/wisewrong/p/10396002.html

进制转换

进制前缀:
0B 表示二进制的前缀
0O 表示八进制的前缀
0x 这个是十六进制的前缀

parseInt(0b10)也可以parseInt(10,2)
parseFloat(0b10)只能这样
1.进制也就是进位计数制,认为定义的带进位的技术方法
2.十六进制是逢十六进一,十进制是逢十进一,八进制是逢八进一,二进制是逢二进一...
3.在javaScript中进制之间的转换提供了两个非常好的方法:toString()、parseInt().
使用方法:
    1.使用toString()方法把十进制转换成其他进制的字符串:
        var x=28;//10进制
        console.log(x.toString(2));//把十进制转为2进制的字符串
        console.log(x.toString(8));//把十进制转为8进制的字符串
        console.log(x.toString(16));//把十进制转为16进制的字符串,x.toString()的x不能直接写数字

    2.使用parseInt()方法把其它进制转为十进制:
        var x="110"//二进制的字符串
        console.log(parseInt(x,2));//把这个字符串当做二进制,转为十进制
        var x="070";//八进制的字符串
        console.log(parseInt(x,8));//把这个字符串当做八进制,转为十进制
        var x='0x1c';////十六进制的字符串
        console.log(parseInt(x,16));//把这个字符串当做十六进制,转为十进制// 其他进制转十进制
        // console.log( parseInt('123a') );//123
        // console.log( parseInt('123a',10) );//123
        var num = '11000';//二进制
        console.log( parseInt(num,2) );//24

        // var num2 = '0x1c';//16进制
        var num2 = '1c';//16进制
        console.log( parseInt(num2,16) );//28

        // 其他进制转十进制
        // console.log( parseInt('123a') );//123
        // console.log( parseInt('123a',10) );//123
        var num = '11000';//二进制
        console.log( parseInt(num,2) );//24

        // var num2 = '0x1c';//16进制
        var num2 = '1c';//16进制
        console.log( parseInt(num2,16) );//28

    其他进制转十进制
        console.log( parseInt('123a') );//123默认为转成十进制
        console.log( parseInt('123a',10) );//123

运算符

就是在代码里面进行运算的时候使用的符号,不光只是数学运算,我们在"js面还有很多的运算方式"
	如: 
        n++ ++n --n  n--;  
        +'a' // 一元运算符  
        n+m n*m // 二元运算符  
        boolean ? express1 : express2 // 三元运算符
        4 > 5 ? alert('aa') : alert('bb') // 三元运算符

    简单说,参与计算的数,有几个,就叫几元运算符
    优点:代码少  缺点:不方便阅读和维护

    var a = 2;
    var b = 3;
    var num = a > b ? 10 : 20;
    console.log(num); // 20 

    var num = a > b ? 10 : ( a == b ? 20 : 30);
    console.log(num); // 30

数学运算符

'+'有两个意义
数学运算加法:当运算符两边, 都是数字或者布尔的时候, 就会进行数学运算
字符串拼接:只要符号任意一边是字符串类型,就会进行字符串拼接

'-'
会执行减法运算
会自动把两边都转换成数字进行运算
不能得到结果就是一个 NaN

'*'
会执行乘法运算
会自动把两边都转换成数字进行运算
不能得到结果就是一个 NaN

'/'
会执行除法运算
会自动把两边都转换成数字进行运算
不能得到结果就是一个 NaN

'%'
会执行取余运算
会自动把两边都转换成数字进行运算
不能得到结果就是一个 NaN

'**'
指数运算
运算是右结合
不能得到结果就是一个 NaN
2**3**2 // 2**(3**2)

数学赋值运算符

数学赋值运算符又称赋值运算符

'='
就是把=右边的赋值给等号左边的变量名
var num = 100
就是把 100 赋值给 num 变量
那么 num 变量的值就是 100

'+='
var a = 10;
a += 10;
console.log(a); //=> 20
a += 10等价于a = a + 10

'-='
var a = 10;
a -= 10;
console.log(a); //=> 0
a -= 10等价于a = a - 10

'*='
var a = 10;
a *= 10;
console.log(a); //=> 100
a *= 10等价于a = a * 10

'/='
var a = 10;
a /= 10;
console.log(a); //=> 1
a /= 10等价于a = a / 10

'%='

var a = 10;
a %= 10;
console.log(a); //=> 0
a %= 10等价于a = a % 10

'**='
let a = 2;
a **= 2;
console.log(a); //=> 4
a **= 2等同于 a = a **2;

逻辑赋值运算符

'||=' // 或赋值运算符
x ||= y 等同于 x || (x = y)

'&&=' // 与赋值运算符
x &&= y 等同于 x && (x = y)

'??=' // Null 赋值运算符
x ??= y 等同于 x ?? (x = y)

'它们的一个用途'是,为变量或属性设置默认值。	
    // 老的写法
    user.id = user.id || 1;
    // 新的写法
    user.id ||= 1;

比较运算符

比较运算符又叫关系运算符
关系运算符比较的情况(特殊有三种)
    1.表达式两侧都是数值--正常比较
    2.表达式两侧都是字符--正常比较(先比较第一个字符,如无第二个字符则停止比较直接出结果),比较字符的ASCII码值,`0`--48,`A`--65,`a`--97
    3.表达式两侧有一侧是纯数字字符串,该字符串自动转成数值(并非码值,仅仅是“值”),在进行比较
    4.表达式两侧有一侧是非数字字符串,不能正常比较,所以非正常比较都返回false
	详细的件"运算规则和比较规则"目录标题
注意:这里所说的值是Number值(包括各种进制的数如16进制的abcdef)

___________________________________________

'=='
等于==只比较值是否相等(忽略类型)
比较符号两边的值是否相等,不管数据类型
'abc'=='abc'
'abc'==abc//不可,abc是个什么东西?啥都不是!!
1 == '1'
两个的值是一样的,所以得到true

'==='
全等===先比较类型(内存地址),在比较值
比较符号两边的值和数据类型是否都相等
1 === '1'
两个值虽然一样,但是因为数据类型不一样,所以得到false
只有两边数据类型一样的时候, 才会去比较值
console.log({}==={})//false(复杂数据类型比较的是引用地址)

'!='
比较符号两边的值是否不等,不管数据类型
1 != '1'
因为两边的值是相等的,所以比较他们不等的时候得到false
a!=undefined/null   //true

'!=='
比较符号两边的数据类型和值是否不等
1 !== '1'
因为两边的数据类型确实不一样,所以得到true
只有两边数据类型一样的时候, 才会去比较值
a !== undefined | null //true

'>='
比较左边的值是否大于或等于右边的值,不管数据类型
1 >= 1结果是true
1 >= 0结果是true
1 >= 2结果是false

'<='
比较左边的值是否小于或等于右边的值,不管数据类型
1 <= 2结果是true
1 <= 1结果是true
1 <= 0结果是false

'>'
比较左边的值是否大于右边的值,不管数据类型
1 > 0结果是true
1 > 1结果是false
1 > 2结果是false

'<'
比较左边的值是否小于右边的值,不管数据类型
1 < 2结果是true
1 < 1结果是false
1 < 0结果是false

逻辑运算符

"逻辑运算符针对的是非二进制的值",针对二进制的运算符见目录"位运算符"
自己:默认进行布尔操作确认是否为 true \ false

'&&'
进行 且 的运算
符号左边必须为true并且右边也是true,才会返回true
只要有一边不是true,那么就会返回false
true && true结果是true
true && false结果是false
false && true结果是false
false && false结果是false


短路操作(非正常操作)
通往天堂的钥匙

var a = 4;
(a < 5) && (a = 6);console.log(a);//a=6
(a > 5) && (a = 6);console.log(a);//a=4  前面已经为false就没必要执行后面的了已经确定false了所以。

var b = 1&&2;
console.log(b); // 2

'||'
进行 或 的运算
符号的左边为true或者右边为true,都会返回true
只有两边都是false的时候才会返回false
true || true结果是true
true || false结果是true
false || true结果是true
false || false结果是false

短路操作(非正常操作)
通往天堂的钥匙
var a = 4;
(3 > 5) || ( a = 5 );console.log(a);//4
(3 < 5) || ( a = 5 );console.log(a);//5

var b = 3 > 4 || 456;console.log(b);//456
var b = 2 || 456;console.log(b);//2

'!'
进行 取反 运算
本身是 true的,会变成false
本身是 false的,会变成true
!true结果是false
!false结果是true

console.log( !1 );//false
console.log( !0 );//true
console.log( !'abc' );//false
console.log( !'');//true  
console.log( !' ');//false
console.log( !!'' );//false

Null \Undefined判断运算符

?? 判断运算符,只有在左侧属性值为null或undefined时,右侧才会生效;
	'这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值'
	let obj = {a:1}
	const num = obj.c ?? 2
    console.log(num) // 2

	'注意:??本质上是逻辑运算,它与其他两个逻辑运算符&&和||有一个优先级问题,现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。'
	// 报错
    lhs && middle ?? rhs
    lhs ?? middle && rhs
    lhs || middle ?? rhs
    lhs ?? middle || rhs

    上面四个表达式都会报错,必须加入表明优先级的括号。
    (lhs && middle) ?? rhs;
    lhs && (middle ?? rhs);
    (lhs ?? middle) && rhs;
    lhs ?? (middle && rhs);
    (lhs || middle) ?? rhs;
    lhs || (middle ?? rhs);
    (lhs ?? middle) || rhs;
    lhs ?? (middle || rhs);

链判断运算符

?. 运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined,如果是直接返回undefined,不在执行?.后面的部分
    const firstName = message?.body?.user?.firstName || 'default';
    const fooValue = myForm.querySelector('input[name=foo]')?.value
    
    '链判断运算符?.有三种写法'
        obj?.prop // 对象属性是否存在
        obj?.[expr] // 同上
        func?.(...args) // 函数或对象方法是否存在	
		'注意:括号是有影响的,如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响'
		如: (a?.b).c,如果a为null或者undefined则(a?.b)直接返回undefined,但是后续.c依旧会执行,就是undefined.c就会报错

	'不支持的写法,以下均会报错'
        // 构造函数
        new a?.()
        new a?.b()
        // 链判断运算符的右侧有模板字符串
        a?.`{b}`
        a?.b`{c}`
        // 链判断运算符的左侧是 super
        super?.()
        super?.foo
        // 链运算符用于赋值运算符左侧
        a?.b = c

	'其他' 右侧不得为十进制数值
    为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。

逗号运算符

关键点:逗号操作符,如果是整体的话(就是被括号包裹住)总是返回最后一个操作数

console.log(1,2,3); // 1 2 3
console.log((1,2,3,4)); // 4
console.log(7<6,4<9); // false  true
console.log((7<6,4<9)); // true

var k=0;
for(var i=0, v=0; i<6,v<9; i++, v++){
    k = i + v;
}
console.log(k); // 16

自增自减运算符

++

'++' (可以说是一元运算符)
进行自增运算,分成两种,前置++和后置++

前置++会先把值自动 +1,在返回
var a = 10;
console.log(++a);
会返回 11,并且把 a 的值变成 11

后置++,会先把值返回,在自动+1
var a = 10;
console.log(a++);
会返回 10,然后把 a 的值变成 11


'--' (可以说是一元运算符)
进行自减运算
分成两种,前置--和后置--
和++运算符道理一样

位运算符

1."位运算只对整数起作用",如果一个运算不是整数,会自动转为整数后再运行。虽然在Javascript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数,运算结果再转化为JS数字。
2.这种位转换使得在对特殊的NaN和infinity值应用位操作时,这两个值会被当成0来处理。"如果是对非数值应用位操作符时,会先使用Number()方法将值转换成数值再应用位操作,得到的结果是一个数值"。
3.逻辑位运算符与逻辑运算符的运算方式是相同的,但是针对的对象不同。"逻辑位运算符针对的是二进制的整数值,而逻辑运算符针对的是非二进制的值。"

位运算符有 7 个,分为两类:
	"位非(NOT),按位与(AND),按位或(OR),按位异或(XOR),左移,有符号右移,无符号右移。"
    逻辑位运算符:位与(&)、位或(|)、位异或(^)、位非(~)
    移位运算符:左移(<<)、有符号右移(>>)、无符号右移(>>>)
	
" & 运算符(位与)" 用于对两个二进制操作数逐位进行比较,并根据下表所示的换算表返回结果
    第一个数的位值	第二个数的位值	运算结果
    1	1	1
    1	0	0
    0	1	0
    0	0	0

	console.log(12 & 5);  //返回值4
	过程解析:
        12二进制表示: 1100 
        5二进制表示:  0101
        两位数从尾部一一进行比较得出100,就是4 (根据上面的换算表进行比较)
    
" | 运算符(位或)" 用于对两个二进制操作数逐位进行比较,并根据如表格所示的换算表返回结果
    第一个数的位值	第二个数的位值	运算结果
    1	1	1
    1	0	1
    0	1	1
    0	0	0

	onsole.log(12 | 5);  //返回值13
	过程解析:
        12二进制表示: 1100 
        5二进制表示:  0101
        两位数从尾部一一进行比较得出1101,就是13 (根据上面的换算表进行比较)

" ^ 运算符(位异或)" 用于对两个二进制操作数逐位进行比较,并根据如表格所示的换算表返回结果(可以看做是逻辑运算符&&的反向操作)
    第一个数的位值	第二个数的位值	运算结果
    1	1	0
    1	0	1
    0	1	1
    0	0	0

    console.log(12 ^ 5);  //返回值9
    过程解析:
        12二进制表示: 1100 
        5二进制表示:  0101
        两位数从尾部一一进行比较得出1001,就是9 (根据上面的换算表进行比较)

	"特殊用法" ^有一个特殊运用,连续对两个数a和b进行三次异或运算,a^=b,b^=a,a^=b可以互换它们的值。这意味着,使用^可以在不引入临时变量的前提下,互换两个变量的值
        let a = 10, b = 11;
        a ^= b, b ^= a, a ^= b;
        console.log(a, b); // 11 10

" ~ 运算符(位非)" 操作它可以返回数值的反码,其本质是操作数的负值减1.对一个整数两次按位非得到它的自身, 对于小数两次按位非可以得到取整的效果。
        let n = 9;
        let i = 9.9;
        let m = ~n;
        console.log(m); // -10
        console.log(~~n); // 9
        console.log(~~i); // 9
        console.log(~12); // -13
        console.log(~5.5) // -6


" << 左移" 左移操作符由两上小于号(<<)表示,这个操作符会把数值的所有位向左移动指定的位数。
    将数值3(二进制为11)向左移动5位,结果就是96
        let n=3;
        let m=n<<5;
        console.log(n.toString(2)); // 11
        console.log(m); // 96

" >> 有符号右移" 将数值向右移动,但保留符号位。在符号的右移操作与左移操作正好相反。
    let n = 96;
    console.log(n >> 5); // 3

" >>> 无符号右移"
1.无符号右移的结果和有符号右移的结果相同
2.无符号右移操作符会把负数的二进制码当成正数的二进制码且由于负数以其绝对值的二进制补码形式表示,所以会导致无符号右移后的结果非常的大
    let n = -96;
    console.log(n >>> 5);// 134217725
	过程分析:
        得到96的二进制,从而得到-96的二进制表示
        0000 0000 0000 0000 0000 0000 0110 0000
        计算二进制反码
        1111 1111 1111 1111 1111 1111 1001 1111
        在二进制反码上加1得到
        1111 1111 1111 1111 1111 1111 1010 0000
        向右移动5位
        0000 0111 1111 1111 1111 1111 1111 1101
        结果就是console.log(0b0000 0111 1111 1111 1111 1111 1111 1101) // 134217725
        
        
'其他'
负数可以存储为二进制代码,不过采用的形式是二进制补码,计算数字二进制补码的步骤如下:
    1、确定数字的非负版本的二进制表示
    2、求得二进制反码后要把0替换为1,1替换为0
    3、在二进制反码上加1
    如:确定-10的二进制补码
        1、10的二进制表示如下:0000 0000 0000 0000 0000 0000 0000 1010
        2、把0替换1,1替换0: 1111 1111 1111 1111 1111 1111 1111 0101
        3、在二进制反码上加1
        
知识借鉴:
        https://blog.51cto.com/u_16099301/6443736
        https://c.biancheng.net/view/5469.html

使用场景

利用<<来实现乘法运算;利用>>实现除法运算;利用^来实现值互换;小数取整

左移是乘法,移动的位数是2的(幂数 × +1)
右移是除法,移动的位数是2的(幂数 × -1)

console.log(4 << 1);//8 
console.log(4 << 2);//16 
console.log(4 << 3);//32 
console.log(4 << 4);//64 
console.log(4 << 5);//128 
console.log(4 << 6);//256  

console.log(5 >> 3);//0
console.log(14 >> 2);//3
console.log(225 >> 5);//7

let a = 20, b = 4;
a ^= b, b ^= a, a ^= b;
console.log(a, b);//4 20

console.log(~~9.9);//9
console.log(9.8 | 0);//9
console.log(9.7 ^ 0);//9
console.log(9.6 << 0);//9
console.log(9.3 >> 0);//9


更多使用场景详见:https://blog.csdn.net/Umbrella_Um/article/details/102874300

点运算符

1.点运算符后面总是字符串

void运算符

void运算符对给定的表达式进行求值,然后返回 undefined,安全起见,当函数返回值不会被使用到的时候,应该使用 void 运算符

语法:void expression,void运算符通常只用于获取 undefined的原始值,一般使用void(0)。

'示例'
const output = void 1;
console.log(output);
// Expected output: undefined

void console.log('expression evaluated');
// Expected output: "expression evaluated"

void (function iife() {
  console.log('iife is executed');
})();
// Expected output: "iife is executed"

'应用'
1.立即调用的函数表达式
    普通的立即执行函数:
        (function fn(){
            console.log(123)
        })()

    用void表示:
        void function fn(){
            console.log(123)
        }()
2.在a链接中阻止跳转
<a href="javascript:void(0);"></a> 
void(0)会返回undefined,这个链接点击之后不会做任何事情,此时就禁止了页面跳转。但是这样不是所有浏览器都兼容的,很多时候我们用href="#"

href="#"和href="javascript: void(0)"的区别 
两者都是阻止页面跳转,href="#"执行的时候会在地址栏后面添加#号,还会让页面的滚动条滚动到页面的最上面。
注意:利用 javascript: 伪协议来执行js代码是不推荐的,利用href="#"也有地址栏添加#号的问题,推荐的做法是为链接元素绑定事件。

3.在箭头函数中
箭头函数标准中,当函数返回值是一个不会被使用到的时候,应该使用 void运算符,来确保返回 undefined。
const fn = () => void doSomething();

4.判断值是否为undefined中
在实际开发中,我们判断一个值为undefined的时候,会第一时间想到txt === undefined的例子,但是这样做其实是有bug的,如果一个全局变量也叫undefined(变量名取名为undefined的基本上是不会有的),那么此时会发生判断错误,正确的写法应该是txt === void(0)
    if(txt === undefined)
    if(txt === void(0))

知识借鉴: 
    1.https://www.jianshu.com/p/30a582b6a8cd
    2.https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/void

程序三大结构

选择结构和循环结构使用看具体情况
1. if()更多时候用在范围的判断, switch()一般用作确定的几个值得判断

顺序结构

按照书写代码顺序进行运行的代码就是顺序结构的

选择结构

1. if else 语句
// 条件为 true 的时候,会执行 if 后面的 {} 
if (true) {
  alert('因为条件是 true,我会执行')
} else {
  alert('因为条件是 true,我不会执行')
}
// 条件为 false 的时候,会执行 else 后面的 {}
if (false) {
  alert('因为条件为 false,我不会执行')
} else {
  alert('因为条件为 false,我会执行')
}


2. if else if ... 语句
可以通过 `if` 和 `else if` 来设置多个条件进行判断
语法:`if (条件1) { 条件1为 true 的时候执行 } else if (条件2) { 条件2为 true 的时候执行 }`
会从头开始依次判断条件
如果第一个条件为 `true` 了,那么就会执行后面的 `{}` 里面的内容
如果第一个条件为 `false`,那么就会判断第二个条件,依次类推
多个 `{}` ,只会有一个被执行,一旦有一个条件为 `true` 了,后面的就不在判断了

// 第一个条件为 true,第二个条件为 false,最终会打印 “我是代码段1”
if (true) {
  	alert('我是代码段1')
} else if (false) {
	alert('我是代码段2')           
}
// 第一个条件为 true,第二个条件为 true,最终会打印 “我是代码段1”
// 因为只要前面有一个条件满足了,就不会继续判断了
if (true) {
  	alert('我是代码段1')
} else if (true) {
  	alert('我是代码段2')
}
// 第一个条件为 false,第二个条件为 true,最终会打印 “我是代码段2”
// 只有前一个条件为 false 的时候才会继续向后判断
if (false) {
  	alert('我是代码段1')
} else if (true) {
  	alert('我是代码段2')
}
// 第一个条件为 false,第二个条件为 false,最终什么也不会发生
// 因为当所有条件都为 false 的时候,两个 {} 里面的代码都不会执行
if (false) {
  	alert('我是代码段1')
} else if (false) {
  	alert('我是代码段2')
}

循环结构

循环结构关键字:
	'break'关键字使用在循环中,他代表终止并跳出循环。
	'continue'关键字使用在循环中,他代表跳过本次循环。
    '循环结构均可以使用以上关键字,实现跳过某次循环或退出循环'

Switch 条件语句

条件判断语句的一种,是对于某一个变量的判断,要判断某一个变量'等于'某一个值得时候使用
var week = 1
switch (week) {
  case 1:
    alert('星期一')
    break
  case 2:
    alert('星期二')
    break
  case 3:
    alert('星期三')
    break
  case 4:
    alert('星期四')
    break
  case 5:
    alert('星期五')
    break
  case 6:
    alert('星期六')
    break
  case 7:
  
  
    alert('星期日')
    break
  default:
    alert('请输入一个 1 ~ 7 之间的数字')
}

default:可选的,不一定每一个switch都要写default,如果前面的值一个都不相等,就执行这里的代码
default下的语句不加{}
default语句一定是放在所有case之后的,但是也可以不写

for语句

for(表达式1;表达式2;表达式3){
    循环体;
}

表达式1:为了不参与循环的单次表达式,用来给循环控制变量赋初值
表达式2:一般是一个关系表达式,作为循环条件(设置终止值)
表达式3:一般为循环变量增量或减量(步长)
循环体:需要重复执行的代码

for(var i=0;i<5;i++){//增量循环
	console.log(i);
}
for(var i=0;i<5;i--){//减量循环
	console.log(i);
}

while循环

while循环只要指定条件为true,循环就可以一直执行代码。
while(条件){
	// 需要执行的代码
}

do/while循环

'do/while循环是while循环的变体'
先执行一次 do{} 代码块,再执行 while() 判断。

for、while和do-while的区别

while循环先是判断,再执行,有可能一次都不执行。
'do/while循环先是执行,在判断,至少执行一次代码块'
for循环一般用在循环次数可以确定的情景。
while循环一般用在循环次数未知的情景。

for...in语句

for(键 in 数据)  主要用于循环遍历对象的属性
数据:对象、数组、字符其他的
循环体:需要重复执行的代码

for...of语句

for(键 in 数据)  主要用于遍历数组或有迭代器对象的值
数据:数组、有迭代器的对象
循环体:需要重复执行的代码

with语句

"with的语法结构为:""
    with(object) {
    /* 语句 */
    }

"with主要是用来对对象取值"
    with(obj) {
        var newa = a;
        var newb = b;
        console.log(newa+newb);
    }

    该语句等价于:

    var newa = obj.a;
    var newb = obj.b;
    console.log(newa+newb);

"with的 优点:""
	当with传入的值非常复杂时,即当object为非常复杂的嵌套结构时,with就使得代码显得非常简洁。

"with的缺点:"
	js的编译器会检测with块中的变量是否属于with传入的对象, 上述例子为例,js会检测a和b是否属于obj对象,这样就会的导致with语句的执行速度大大下降,性能比较差。

知识借鉴:https://blog.csdn.net/ljh101/article/details/111402450

语句作用域

语句没有自身作用域,语句展示:
    顺序结构语句
    选择结构语句
    循环结构语句
    for in 语句
    for of 语句

以上这些语句都不存在'自身作用域',函数存在自身作用域,详情见下方例子
	'if语句'
    if (true) {
        var b = 3;
    }
    console.log(b) // 3
    // 依旧可以访问到,所以不存在自身作用域

	'for...of语句'
	var arr = [1,2,3];
    for (var value of arr) {
        var b = 3
        console.log(value)
    }
    console.log(b) // 3 可以访问到

	'函数'
    function fn () {
        var a = 3;
    }
    fn()
    console.log(a); // Uncaught ReferenceError: a is not defined

循环语句和循环函数

'循环执行的函数'
	let docs = [{}, {}, {}];
    docs.reduce(async (_, doc) => {
      console.log('执行了')
      await _;
      await doc;
      console.log('await')
    }, undefined);
    console.log('Haha')
    // 执行了 * 3
    // Haha
    // await * 3
	// 上方将相当于执行了三次对应的异步函数

'循环执行的语句'
    let docs = [{}, {}, {}];
    async function fn () {
      for (var value of docs) {
        console.log('执行了')
        await value;
        console.log('await')
      };
      console.log('Haha')
    }
    fn();
    // 执行了
    // await
    // 执行了
    // await
    // 执行了
    // await
    // Haha
	// 上方将相当于执行了三次对应的循环体

词法作用域和动态作用域

`JavaScript采用的是静态作用域`

"词法作用域": 大部分的语言都是词法作用域,包括js,词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则,词法作用域最重要的特征是它的定义过程发生在代码的书写阶段

function foo() {
    console.log(a) // 2  词法作用域让foo()中的a通过RHS引用到了全局作用域中的a,因此会输出2
}

function bar() {
    var a=3;
    foo();
}
var a = 2;
bar();


"动态作用域": 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。this机制某种程度上很像动态作用域。但是箭头函数里的this反而像词法作用域

"总结": 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

知识借鉴:https://blog.csdn.net/qq_43485006/article/details/112334724

函数

通俗的讲:函数就是可重复执行的代码。
函数的作用:
    1.通过函数可以封装任意多条语言,以便在任何地方、任何时候调用
    2.将代码编写在函数里,就可以避免在非必要情况下调用该代码。
    
知识借鉴:https://es6.ruanyifeng.com/#docs/function

函数的编写及调用

JavaScript中的函数使用function关键字来声明,后跟一组参数及函数体。
最基本的函数声明及调用
function 函数名(参数){//函数体
    // do something
}
函数需要调用才能触发:函数名(参数);//调用
fnName(); // 函数调用
console.log( fnName ); // 访问一个变量(函数变量名),相当于访问整个函数
console.log( fnName() ); // 打印函数的返回值 函数默认返回值为undefined(调用之后才会有返回值)

`函数分为系统函数和自定义函数`
1.系统函数:alert()、parseInt()、prompt()......

2.自定义函数:
    函数声明、函数表达式、匿名函数
    注意:函数声明(任意位置调用),函数表达式(先定义后调用),匿名函数(不允许单独定义)
    调用:只要能定位到这个函数再在后面加小括号就可以调用

    1.函数声明
    function fn1(){
     console.log('函数声明')
    }
    fn1(); // 函数调用(放在前面也可以调用)

    2.函数表达式(不能在定义之前调用)
    var fn2=function (){
    console.log('函数表达式')
    }
    fn2(); // 函数调用,原因是因为这里的fn2=function(){}是要到执行阶段才会赋值给fn2的,所以放在前面会报错显示这不正一个函数

    3.匿名函数(不允许单独定义)
    function (){ // Uncaught SyntaxError: Function statements require a function name
         console.log('匿名函数');
     }
    var box = document.querySelector('.box');
    box.onclick = function (){ // 事件处理函数(匿名函数)
        alert('BMW');
    } 

自调用函数

'可以通过在关键字前加一个一元运算符来实现,它只接受表达式作为操作数。函数调用的优先级比一元运算符高,所以它将被首先执行'

自调用函数 | 匿名自执行函数 | 立即执行函数(IIFE)
作用及优点:
    独立的作用域,不会污染全局环境,模块化
    初始化数据和页面(只执行一次)
    解决闭包中的状态保留问题
   

如:
    (function (a,b){
        console.log(a+b);
    })(2,3);
	// 圆括号的作用是强迫 function 关键字被解析为表达式的开始,而不是语句

常见形式:
;(function () {
    console.log(11);
})();

;(function () {
    console.log(11);
}());

!function () {
    console.log(11);
}();

+function () {
    console.log(11);
}();

知识借鉴:https://blog.csdn.net/ymjring/article/details/40889257

函数和普通变量存储方式的区别

lenght属性

函数的length属性,返回函数的形参个数
实例:根据传递的参数,执行对应的加减乘除运算

function count(a,b,c){
   console.log(a,b,c);
}
console.log(count.length) // 3

函数的返回值

1.所有函数都有返回值!
2.如果函数没有明确的返回值,默认返回undefined,使用return语句可以自定义返回值。
    return语句会终止当前函数的执行并返回函数的值。(直接终止函数不管函数内是否还有语句)
    return 关键字可以在函数运行完成以后返回一个值,返回到了函数的调用位置
	注意:函数内,return语 句之后的所有代码都不会执行!

函数的返回值
function fun(){
    console.log('hello world');
}
var val = fun();
console.log( val ); // undefined
console.log( fun() ); // undefined


function test (a, b) {
    if (a == b) {
        return;
    }
    return '两者不相等'
}
var num = test(2, 4)
console.log(num) // '两者不相等'

函数参数

未设置默认值

var a = 3
function foo(a) {
   console.log(a) // undefined
}; foo()
// 从上面可以看出foo(a)相当于成了下面这个
 function foo(a) {
     var a = undefined; // 待执行时赋值,如: foo(3),就相当于 a = 3
	console.log(a)
 }


考察示例:
var a = 3
function foo(a) {
    console.log(a) // undefined
    a = 4
    console.log(a) // 4
}; foo()

var a = 3
function foo(a) {
    console.log(a) // undefined
    var a = 4
    console.log(a) // 4
}; foo()

var a = 3
function foo(a) {
    console.log(a) // 2
    var a = 4
    console.log(a) // 4
}; foo(2)

设置默认值

详细见目录标题"作用域"
"设置了默认值的参数,在函数执行的时候,先执行函数参数,然后再执行函数体"

参数变量是默认声明的,所以不能用let或const再次声明。
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}
// ES6规定在某个区块中, 一旦用let或const声明一个变量,那么这个区块就变成块级作用域,用let 或者const声明的变量即为该区块绑定, 该变量不受任何变量影响。 在该变量使用let声明前不可以用。在语法上,我们叫这种情况为:暂时性死区 

使用参数默认值时,函数不能有同名参数(跟设置了默认值后形成作用域有关)
// 不报错
function foo(x, x, y) {
  // ...
}

// 报错
function foo(x, x, y = 1) {
  // ...
}

如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

解构赋值

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // undefined 5


——————————————————————————————————————————————————————

let f = 3
function foo({x, y = f} = {}) {
  console.log(x, y); // undefined 3
};foo()

参数默认值的位置

1.头部未省略:中间不可省略
2.头部省略:无其他参数则正常,有的话报错

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]

参数设置默认值后length 属性

1.函数length计算到设置了默认值的参数就会停止
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

作用域

1.一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。单独形成的作用域就类似(见下方){let x, let y = x}这种,当函数内部预解析完后就会消失变成普通的类似var x, var y = x

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
"解析:在执行函数f(2)时首先进行预解析,预解析时因为函数参数设置了默认值,所以会单独形成一个作用域,就类似成了这样{let x, let y = x},因为设置了默认值的参数,在函数执行的时候,先执行函数参数,然后再执行函数体,此时y=x,当传入x=2时y也就是2了"
上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。

再看下面的例子。

let x = 1;

function f(y = x) {
  var x = 2;
  console.log(y);
}

f() // 1
"解析:在执行函数f()时首先进行预解析,预解析时因为函数参数设置了默认值,所以会单独形成一个作用域,就类似成了这样{let y = x},因为是默认值,所以会直接赋值y=x,因为设置了默认值的参数,在函数执行的时候,先执行函数参数,然后再执行函数体,所以x直接找到外部的x值为1"
上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。

如果此时,全局变量x不存在,就会报错。

function f(y = x) {
  var x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined
下面这样写,也会报错。

var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined
上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。


var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1
上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。

函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

如果将var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1

rest参数

 rest 参数(形式为...变量名)

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用


2.注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错
function f(a, ...b, c) {
  // ...
}


3.函数的length属性,不包括 rest 参数。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

设置严格模式后

ES2016规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

// 报错
function doSomething(a, b = a) {
  'use strict';
  // code
}

// 报错
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// 报错
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 报错
  doSomething({a, b}) {
    'use strict';
    // code
  }
};
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

// 报错
function doSomething(value = 070) {
  'use strict';
  return value;
}
上面代码中,参数value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。

虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

'use strict';

function doSomething(a, b = a) {
  // code
}
第二种是把函数包在一个无参数的立即执行函数里面。

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

箭头函数

1.由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

2.如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();
箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}


function full({person, my}) {
  console.log(person,my);
}
full({person:4,my:5}); // 4 5

注意事项

箭头函数有几个使用注意点。

(1)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(2)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(3)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
setTimeout(() => console.log(s2,4), 3100);
function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
// 使用 严格模式
var a=3,b=5;
function args(a,b){
    'use strict'
    arguments[0]=4
    console.log(a,b)

}
args(a,b)
console.log(a,b)


——————————————————————————————————————————

// 未使用 严格模式
var a={c:3},b={d:4};
function args(a,b){

    // 'use strict'
    arguments[0]=4
    console.log(a,b)

}
args(a,b)
console.log(a,b)

//未用use strick模式是arguments是可以改变传入的值得,使用了use strick 时是改变不了的

递归函数

递归函数在运行的时候,每调用一次函数就会在内存中开辟出一块空间,内存消耗较大,注意防止栈溢出。

程序调用自身的编程技巧称为递归( recursion)。
递归,就是在运行的过程中调用自己,本质就是循环。

满足以下特点就是递归:
   1,函数自己调用自己
   2,一般情况有参数
   3,一般情况有return
   方法:
     1,首先去找临界值,即无需计算,获得的值
     2,找这一次和上一次的关系
     3,假设当前函数已经可以使用,调用自身计算上一次

构成递归需具备的条件
   1. 子问题须与原始问题为同样的事,且更为简单;
   2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。

由于递归是函数本身一层一层压栈,导致先入栈的不能出栈,空间占满以后就会造成堆栈溢出

 斐波那契数列**指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89........

    这个数列从第3项开始,每一项都等于前两项之和。

    在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>2,n∈N*)
    
    
    // 阿里巴巴2015年前端面试题
/**
*@desc: fibonacci
*@param: count {Number}
*@return: result {Number} 第count个fibonacci值,计数从1开始
  fibonacci数列为:[1, 1, 2, 3, 5, 8, 13, 21, 34 …]
  getNthFibonacci(2) 返回值为1
  getNthFibonacci(6) 返回值为8
*/
// f(7) = f(6)+f(5)
// f(6) = f(5)+f(4)
// f(5) = f(4)+f(3)
// f(4) = f(3)+f(2)
// f(3) = f(2)+f(1)
// f(2) = 1
// f(1) = 1

// f(n) = f(n-1)+f(n-2)
// 终止条件:n <= 2   return 1;
function getNthFibonacci(n){
    if (n <= 2) {
        return 1;
    }
    return getNthFibonacci(n-1) + getNthFibonacci(n-2);
}
console.log( getNthFibonacci(6) );//8
console.log( getNthFibonacci(2) );//1

</script>

回调函数

<script>
function fn1(callback){
    var a = 3;
    console.log('fn1执行');
    // fn2(a);
    // fn3(a);
    callback(a);//fn2(a);  fn3(a);
}
function fn2(x){//回调函数
    console.log(x);
}
function fn3(x){//回调函数
    console.log(x+4);
}

fn1(fn2);
fn1(fn3); 

</script>

构造函数

构造函数:用于创建特定类型的对象。
以下是自带的已经定义好的内部构造函数
JS内部构造函数:Object、Number、String、Array、Function、Boolean等等...
当任意一个普通函数用于创建一类对象,并通过new操作符来调用时它就可以作为构造函数。
构造函数一般首字母大写。(并非一定要大写)
构造函数函数对象就是函数,其他的都是对象,这一个要注意。

'js中new操作符具体进行了什么操作'
	首先生成了一个空对象,之后执行new后面的函数,并且将函数内部的this指向了这个生成的空对象,执行完后如果return的为非对象则是生成了这个对应的对象,若return出来的是非null对象则是当成普通函数执行了。
<script>
// 构造函数:用于创建特定类型的对象。(类)

// function test(){
//     console.log(123);
// }
// var val = test();//普通函数调用
// console.log(val);//undefined


// var val = new test();//作为构造函数调用
 console.log(val);
// console.log(typeof val);//object


var str1 = 'abc';
var str2 = String('abc');
var str2 = new String('abc');
// console.log(typeof str1);//string
// console.log(typeof str2);//object
// console.log(str1 == str2);//true
// console.log(str1 === str2);//false


var num1 = 123;
var num2 = Number(123);
var num2 = new Number(123);
// console.log(typeof num1);//number
// console.log(typeof num2);//object
// console.log(num1 == num2);//true
// console.log(num1 === num2);//false


var boo1 = true;
var boo2 = Boolean(true);
var boo2 = new Boolean(true);


var obj1 = {};
var obj2 = Object();
var obj2 = new Object();
var obj2 = new Object({a:7, b:2, c:3});


var arr1 = [];
var arr2 = Array();
var arr2 = new Array();
var obj2 = new Array({a:7, b:2, c:3},[1,2,3]);


var fun1 = function (){ alert('123') };
var fun2 = new Function('alert("abc");console.log(1)');
// fun2();
// console.log( fun1 );
// console.log( fun2 );
// console.log(typeof fun1);//function
// console.log(typeof fun2);//function(函数对象就是function)


// function Human(n,s,a){//构造函数(类)
//     this.name = n;
//     this.sex = s;
//     this.age = a;
//     this.say = function (){
//         alert('hi,大家好');
//     }
// }
// var zhangsan = new Human('张三','女',18);
console.log(typeof zhangsan)//Object
// var lisi = new Human('lisi','女',21);
// console.log( zhangsan.name );
// zhangsan.say();

</script>


var fun2 = new Function('alert("abc");console.log(1)');
fun2.a = 2
console.log(fun2())//undefined  (函数对象就是function)
console.log(fun2)   //???????

如:
var fun2 = new Function('alert("abc");console.log(1); return 1');
fun2.a = 2
console.log(fun2())//1 (函数对象就是function)
console.log(fun2)   //f anonymous(){alert("abc");console.log(1); return 1}
console.log(fun2.a) //2

如:
 function Human(){//构造函数(类)
   
 }
 var zhangsan = new Human();
console.log( zhangsan()) // Uncaught TypeError: zhangsan is not a function
console.log( typeof zhangsan)//Object

尾调用优化

//尾调用优化只在严格模式下开启,正常模式是无效的。

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归函数的改写 => 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120
还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

非尾递归的 Fibonacci 数列实现如下。

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时
尾递归优化过的 Fibonacci 数列实现如下。

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

作用域

1.当全局与局部有同名变量的时候,访问该变量将遵循“就近原则”逐步向外寻找(如果局部变量和全局变量重名,使用的时候优先使用局部变量)

全局作用域

1.整个页面起作用,在<script>内都能访问到;
2.在全局作用域中有全局对象window,代表一个浏览器窗口,浏览器创建,可以直接调用;
3.全局作用域中声明的变量和函数,会作为window对象的属性和方法保存;
4.变量在函数外声明,即为全局变量,拥有全局作用域。


<script>
// 全局作用域
// window:全局对象,浏览器提供
// 访问window对象下的属性方法,可以省略 window. 而直接访问
    
var abc = 123;//全局变量
function myFun(){
    alert(abc); // alert(window.abc)
}
myFun(); // window.myFun();
console.log(abc); // console.log(window.abc);
alert(123); // window.alert(456);

局部作用域

1.在JavaScript中,函数是唯一拥有'自身作用域的代码块'。
2.局部作用域内的变量只能在函数内部使用,所以也叫函数作用域;
3.变量在函数内声明,即为局部变量,拥有局部作用域。
4.因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。


function test(){
  // 局部作用域
  var aaa = '777'; // 局部变量  拥有局部作用域
  var def = '888';
  function fn(){
      alert('局部函数');
      console.log(def); // 456
      console.log(aaa);
  }
  fn();
}
test();
fn(); // Uncaught ReferenceError: fn is not defined
console.log(aaa);//Uncaught ReferenceError: aaa is not defined




function f123(){
    // console.log(yoyo) // 报错
    var hehe = 'haha';//局部变量  显式声明
    yoyo = 1212;//全局变量  隐式声明
    console.log(123456789);
}
f123();
console.log(window.yoyo); // 1212
console.log(yoyo); // 1212
console.log(hehe); // 报错

块级作用域

`ES5 只有全局作用域和函数作用域,没有块级作用域`

1.第一种场景,内层变量可能会覆盖外层变量。

var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined
上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

2.第二种场景,用来计数的循环变量泄露为全局变量。

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

块级作用域与函数声明

`ES6: let/const实际上为 JavaScript 新增了块级作用域`
其他了解:
    1.ES5和ES6是EMACScript的不同版本,不同规则;严格模式是ES5新增的规则
    2.目前浏览器基本上都默认开启了ES6环境
    
`ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。虽然可以不遵守也不会报错,但还是建议遵从` 函数只能在顶层作用域和函数作用域之中声明:就是在全局顶层声明或者函数内部顶层声明

`ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。`

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());


ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?

// 浏览器的 ES6 环境 
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
上面的代码在 ES6 浏览器中,都会报错。

原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

允许在块级作用域内声明函数。
"函数声明类似于var,即会提升到全局作用域或函数作用域的头部。"
"同时,函数声明还会提升到所在的块级作用域的头部。"
"注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。譬如设置了严格模式后就按let处理"

根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。上面的例子实际运行的代码如下。

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

变量的生命周期

1.全局变量在页面打开时创建,在页面关闭后销毁。
2.局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁
3.局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后再函数中使用这些变量,直至函数结束

全局
var i = 0;
function fn1(){
    i++;
    console.log(i);
}
fn1(); // 1
fn1(); // 2
fn1(); // 3

局部
function fn2(){
    var i = 0;
    i++;
    console.log(i);
}
fn2(); // 1
fn2(); // 1
fn2(); // 1

注意:全局变量尽量减少使用,其一不用也占内存,其二造成变量命名的污染(合作写代码有相同全局变量造成功能错误)

堆栈

是一种数据结构,指的是数据存取的方式,当定义一个变量时,内存会开辟一段空间
1. 栈(Stack):先进后出(FILO),在栈顶做插入(压栈)和删除操作(出栈)。
2. 队列:先进先出(FIFO),在队头做删除操作,在队尾做插入操作。
3. 堆和它们不同,代码执行时系统动态分配,不存在是先进后出还是先进先出。

执行环境执行栈(也称执行上下文–execution context): 当JavaScript解释器初始化执行代码时,它首先默认进入全局执行环境,从此刻开始,函数的每次调用都会创建一个新的执行环境,每一个执行环境都会创建一个新的环境对象压入栈中,当执行流进入一个函数时,函数的环境对象就会被压入一个环境栈中(execution stack)。在函数执行完后,栈将其环境弹出,把控制权返回给之前的执行环境。

作用域链

内层环境可以访问外层中的变量和函数,而外层环境不能访问内层的变量和函数
作用域链:会沿着作用域一层一层往外查找,是一个链式结构

this对象

1. this是js的一个关键字,它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。
2. this的执向在函数运行时才进行绑定

<div class="box">一个坑</div>
<script>
    var box = document.querySelector('.box');
	// console.log(this); // window
function show(){
    console.log(this);
}

1、函数的调用对象(谁去调用)
show();
window.show(); // this指向它所在的函数的调用对象(谁去调用)(一般不会指向自身所在的函数)


2.this指向事件的调用对象
box.onclick = function (){ // this指向事件的调用对象
    console.log(this); // box
    // show(); // window
    // window.show(); // window
}


box.onclick = show; // box
box.onclick = window.show; // box
box.onclick = function show(){
    console.log(this); // box
}

</script>

内置对象

内置对象的详情详见"数据类型.md"文件

对象Object

1.对象的概念及操作
	对象是一组无序的键值对,是带有属性和方法的集合。(无序,所以对象没有length属性),通俗讲,对象就是无序的数据集合。属性是与对象相关的值,方法是能够在对象上执行的动作。

2.对象的作用:用于在单个变量中存储多个值。

3. JavaScript 中的所有事物都是对象:字符串、数值、数组、函数...
4. javaScript 中万事万物皆对象

创建对象

1. 所有对象类型都可以动态添加属性和方法!
2. 简单类型不可以动态添加属性和方法!

创建对象的方式:
    1、字面量  var obj = {}
    2、通过new运算符 var obj = new Object()
    3、构造函数
            用来构造(创建)对象的函数
            他在声明的时候跟普通函数没有区别
            用new运算符加上函数的调用,调用的结果就是一个对象
            构造函数中的this指的是即将要new的那个对象
    4、ES6语法糖
    
    
例子:
    1.字面量  var obj = {
        键值对
        key:value
    }

    var xm = { // 对象字面量
        "age": 22, // 一般用双引号引起来(不用引号也可以)
        sex: '男', // 值可以是任意类型的数据
        name: 'xiaoming',
        sayHi: function (){
            alert('大家好,我叫' + xm.name);
        }
    };
    xm.friends = ['xh','xw','xd'];(也可动态添加)


    2. new运算符 var obj = new Object()
        Object 构造函数为给定的参数创建一个包装类对象(object wrapper),具体有以下情况:

            2.1.如果给定值是 null或 undefined,将会创建并返回一个空对象
            2.2.如果传进去的是一个基本类型的值,则会构造其'包装类型'的对象
            2.3.如果传进去的是引用类型的值,仍然会返回这个值,经他们复制的变量保有和源对象相同的引用地址
            2.4.当以非构造函数形式被调用时,Object 的行为等同于 new Object() 
			
			印证 2.3 点:
                var arr = Object([1,2,3]);
                console.log(arr);
                console.log(Object.prototype == arr.__proto__) // false

    3.使用构造函数创建对象
        var obj2 = new Object();
        obj2.name = 'xd';
        obj2.age = 24;

	4. class语法糖
    class obj {
        construct () {
            this.a = 3;
            this.b = 4;
            this.say = function() {console.log(this.a)}
        }
        say() {console.log(this.b)}
        c = 4;
    }

知识借鉴:https://baijiahao.baidu.com/s?id=1728859393620651383&wfr=spider&for=pc

使用new Object()创建对象

image-20230312134220429

删除对象的属性

delete obj.bianmei

JS内置对象

"javaScript的内置对象:系统提供的对象"
1. Object对象    是所有JavaScript对象的超类(基类)
2. Array对象     数组对象--定义数组属性和方法
3. Boolean对象   布尔对象--布尔值相关
4. Date对象      日期对象--日期时间相关
5. Error对象      错误对象--处理程序错误
6. Function对象   函数对象--定义函数属性和方法
7. Math对象      数学对象--各种数学运算工具(不是构造函数)
8. Number对象    数字对象--定义数字属性和方法
9. RegExp对象    正则表达式对象--定义文本匹配与筛选规则
10. String对象    字符串对象--定义字符串属性和方法
...
Number Boolean String Array Function Object Set Map Symbol Math Date Error RegExp ...

访问对象成员

1. 对象.属性  对象.方法()   
2. 对象[变量或属性字符串]


var obj = {
    name: '老王',
    xiaoMing: '小名'
};
var pepelo = 'xiaoMing', who = 'name'
console.log( obj[pepelo] ); // 小名
console.log( obj['name'] ); // 老王
console.log( obj[who] ); // 老王
console.log( xw['na' + 'me'] ); // 老王

遍历对象

1. Object.keys() : 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
var obj = {'0':'a','1':'b','2':'c'};
Object.keys(obj).forEach(function(key){
     console.log(key,obj[key]);
});

2. for in : 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
var obj = {'0':'a','1':'b','2':'c'};
for(var i in obj) {
     console.log(i,":",obj[i]);
}

3. Object.getOwnPropertyNames(obj) : 返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)
var obj = {'0':'a','1':'b','2':'c'};
Object.getOwnPropertyNames(obj).forEach(function(key){
    console.log(key,obj[key]);
});

4.Object.getOwnPropertySymbols(obj) : 返回一个数组,包含对象自身的所有 Symbol 属性的键名。

5. Reflect.ownkeys(obj) : 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]


以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
    首先遍历所有数值键,按照数值升序排列。
    其次遍历所有字符串键,按照加入时间升序排列。
    最后遍历所有 Symbol 键,按照加入时间升序排列。

知识借鉴:
    1.https://www.cnblogs.com/chenyablog/p/6477866.htm
    2.https://es6.ruanyifeng.com/#docs/object#属性的遍历

可枚举和不可枚举

基本包装类型的原型属性是不可枚举的。如:Object,Array,Number,Boolean,Function,String,Math...等系统设定的构造函数自身对象原型属性是不可枚举的,还有对象属性设置了enumerable:false的

Object.getOwnPropertyDescriptor(Math,'abs') 
getOwnPropertyDescriptor只能在对象自身属性上才能生效,访问对象的原型链上的属性是无效的,直接返回undefined
{writable: true, enumerable: false, configurable: true, value: ƒ}

在JavaScript中,对象的属性分为可枚举和不可枚举之分,它们是由属性的enumerable值决定的。可枚举性决定了这个属性能否被for…in查找遍历到(for...in可以循环在Object,Array,Number,Boolean,Function,String  原型上设置的属性,循环不了原型本身自带的属性)。

可枚举的可以使用for...in查找遍历出来
实例对象的propertyIsEnumerable()方法(也就是Object.prototype上的方法)可以判断此对象是否包含某个属性,并且这个属性是否可枚举。需要注意的是:如果判断的属性存在于Object对象的原型内,不管它是否可枚举都会返回false。

如:var obj = {a:3,b:5}
Object.prototype.name=4
Object.prototype.constructor=Object
console.log(obj.propertyIsEnumerable('a')) // 打印 true
console.log(obj.propertyIsEnumerable('name')) // 打印 false
for(let i in obj){
	console.log(i)
}// a  b  name  

知识借鉴:https://blog.csdn.net/qiqi_77_/article/details/79556754
 js中基本包装类型的原型属性是不可枚举的,如Object, Array, Number等,如果你写出这样的代码遍历其中的属性
 如:
 var num =new Number();
 for(let i in num){console.log(i)}//打印时没有任何东西的
 
 
var obj = {a:3,b:5}
Object.prototype.name=4
for(let i in obj){
    console.log(i)
}
//a   b   name ,没有constructor,因为constructor是Obect原型上(Object.prototype)的属性

属性的枚举性会影响以下三个函数的结果:
for…in
Object.keys()
JSON.stringify



function Person() {
    this.name = "KXY";
}
Person.prototype = {
    constructor: Person,
    job: "student",
};
 
var kxy = new Person();
for(let i in kxy){
    console.log(i)
}
//name constructor  job 这里为什么会有constructor,因为这里的constructor并非是Object,Array,Number...等等的原型上的属性,而是Person.prototype上的属性
function Person() {
    this.name = "KXY";
}
Person.prototype = {
    constructor: Person,
    job: "student",
};
 
var kxy = new Person();
Object.defineProperty(kxy, "sex", {
    value: "female",
    enumerable: false
});
for(let i in kxy){
	console.log(i)
}
// name  constructor job //由于使用了enumerable为false所以sex属性是不可枚举的

console.log(Object.keys(kxy)); // ['name'],说明该方法只能返回 对象本身 具有的可枚举属性
console.log(JSON.stringify(kxy)); // 此方法也只能读取 对象本身 的可枚举属性,并序列化为JSON对象。

// 总结:
Object.keys()和JSON.stringify都是只能使用在'对象自身'设置的属性上,无法使用在原型上
for ... in可以用在'对象自身'属性上'也可以'用在'设置的原型属性上',但是原型自带的属性上无法使用(因为原型上自带的属性默认都是不可枚举的);只遍历对象自身的和继承的可枚举的属性

其他:
    for...in循环:只遍历对象自身的和继承的可枚举的属性。
    Object.keys():返回对象自身的所有可枚举的属性的键名。
    JSON.stringify():只串行化对象自身的可枚举的属性。
    Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

"注意: ES6 规定,所有 Class 的原型的方法都是不可枚举的"
var obj = {a:3,b:5}
Object.prototype.name=4
Object.prototype.constructor=Object
Object.defineProperty(obj, "sex", {
    value: "33",
    enumerable:true,
    // configurable:true,
    writable:true,

});
obj.sex = 44
delete obj.sex
console.log(obj,obj.propertyIsEnumerable('a' ))
for(let i in obj){
    console.log(i)
} // 看不懂的话含下面的Object原型方法就可以了看懂了

Object对象上的方法

Object.defineProperty()
1.分为数据属性和访问器属性,数据属性有:value,writable,enumerable,configurable;访问属性有:set,get,enumaerable,configurable。
2.通过使用'对象字面量或者构造函数实例化'对象里的自身属性,其writable,enumaerable,configurable值都会默认是 true,通过Object.defineProperty创建属性时如果没有指定属性的 configurable、enumerable 和 writable 特性,默认值都是 false



一、数据属性详解:
[[Value]] : 包含这个属性的数据值。读取属性时从这个位置读取;写入属性时,把新值保存在这个位置。这个特性的值默认为 undefined 。
[[Writable]] : 表示能否修改属性的值,即值是可写的还是只读。
[[enumerable]] : 目标属性是否可被枚举(遍历)。enumerable表示对象属性是否可以在for…in和Object.keys()中被枚举。
[[Configurable]] : 表示能否通过delete删除属性从而重新定义属性,能否修改属性特性,能否把属性修改为访问器属性。

若需要修改属性默认的特性,必须使用 ES5 的 Object.defineProperty() 方法。它可以定义新属性并设置其特性或者改变属性的原有特性。用法如下

// 三个参数均为必需参数 
Object.defineProperty(obj, prop, attributeDesc);
obj :属性所在的对象(目标对象)
prop :定义的新属性或者修改的属性的名字
attributeDesc : 属性特性描述对象

使用该方法创建或者修改对象属性时主要注意两点:
1.创建属性时如果没有指定属性的 configurable、enumerable 和 writable 特性,默认值都是 false 。
2.一旦将属性的 configurable 特性定义为 false ,即不可配置的,该属性就不能再改回可配置的了。此后,再调用 Object.defineProperty() 方法修改除了 writable 特性的特性都会导致错误。(JavaScript 高级程序设计)

总结一下configurable:当configurable设为false时,
  1、不可以通过delete去删除该属性从而重新定义属性;
  2、不可以转化为访问器属性;
  3、configurable和enumerable不可被修改;(修改会报错)
  4、writable可单向修改为false,但不可以由false改为true;(修改会报错)
  5、value是否可修改根据writable而定。

  
  
二、访问器属性详解:
访问器属性不包含数据值,它们包含两个函数 getter 和 setter(都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数决定了如何处理数据

[[Configurable]] : 表示能否通过delete删除属性从而重新定义属性,能否修改属性特性,能否把属性修改为数据属性。
[[enumerable]] : 表示能否通过for-in 循环访问属性。
[[Get]] : 读取属性时调用的函数,默认为undefined。
[[Set]] : 写入属性时调用的函数,默认为undefined。

var person = {
    name: 'Nicy',
    _age: 21,
    year: 1997,
    _year: 1997,
    sayName: function() {
        console.log(this.name);
    }
}
 
Object.defineProperty(person, "age", {
    get: function() {
        return this._age;
    },
    set: function(value) {
        this._age = value;
                // ...
    }
})




————————————————————————————————————————————————————————————
可以直接Obj = {} 这样写,不用在外面在写var obj = {} 
Object.defineProperty(obj = {}, 'key', {
  enumerable: true,
  configurable: true,
  writable: true,
  value: 9,
})
console.log(obj);

知识借鉴: 
    https://blog.csdn.net/TKOP_/article/details/115018189
    https://blog.csdn.net/u010856177/article/details/127092525
Configurable
[[Configurable]] : 表示能否通过 delete 删除属性、能否修改属性的特性,或者将属性修改为访问器属性。

let a = {};
 Object.defineProperty(a,'a',
{
  value: 123,
  writable: true,
  enumerable: true,
  configurable: true,
})
a.a=5
console.log(a.a)

Object.defineProperty(a,'a',
{
  get(){
    console.log('jljl')
  },
})
console.log(a,a.a)
Enumerable
[[Enumaerable]] : 目标属性是否可被枚举(遍历)。enumerable表示对象属性是否可以在for…in和Object.keys()中被枚举。
如果一个属性的enumerable为false,下面三个操作不会取到该属性:for…in循环,Object.keys方法,JSON.stringify方法

var person = {};
Object.defineProperty(person, "a", { value : 1, enumerable:true });
Object.defineProperty(person, "b", { value : 2, enumerable:false });
Object.defineProperty(person, "c", { value : 3 }); // enumerable defaults to false
person.d = 4; // 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable默认为true
 
for (var i in person) {    
  console.log(i);  
}  //  'a' 和 'd' 
 
console.log(Object.keys(person),1); // ["a", "d"]
console.log(JSON.stringify(person),2) // {"a":1,"d":4}
定义的属性直接是在对象自身身上并不在原型上
const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,//默认是undefined
  writable: false,//不设置则默认为false  属性是否可被改写
  enumerable:true,//不设置则默认为false   属性是否可被枚举
  configurable:true,//不设置则默认为false  属性是否可以被删除
  // get(){},
  // set(){},
});
数据属性与访问器属性的相互转换
Object.getOwnPropertyDescriptor 读取属性的特性
使用Object.getOwnPropertyDescriptor可以获取到属性的描述符:

Object.getOwnPropertyDescriptor(obj, prop)
  obj:属性所在的对象;
  prop:要访问的属性名。

一、数据属性 -> 访问器属性
属性的特性只能是访问器描述符和数据描述符中的一种,给已有的数据属性加get或set转换为访问器属性时,其属性的value、writable就会被废弃。

如下代码,将对象原有的数据属性year转换为访问器属性:

*注:在访问器属性的get和set中,不可以使用this访问属性本身,否则会无限递归而导致内存泄漏。

// 设置get和set其中任意一个即可转换为访问器属性
Object.defineProperty(person, "year", {
    get: function() {
//        return this,year;    // error
        return this._year;    
    },
    set: function(value) {
//             this.year = value;  // error
        this._year= value;
    }
})
 
var descriptor = Object.getOwnPropertyDescriptor(person, 'year');
console.log(descriptor);    // {get: ƒ, set: ƒ, enumerable: true, configurable: true}
在原有的数据属性year中,使用Object.defineProperty()为属性设置get 或 set,都可以将其转换为访问器属性。

二、访问器属性 -> 数据属性
将访问器属性转换为数据属性,只需要给现有访问器属性设置value或writable这两个属性描述符中的任意一个即可,其原有的get和set就会被废弃,从而转换为数据属性。

上面为person定义的访问器属性age,通过Object.defineProperty()只设置了get和set,所以configurable默认为false,不可以将其转换为数据属性。可以在访问器属性和数据属性间相互转化的属性其configurable特性值必须为true。

如下代码,我们为person新定义一个访问器属性job,将其configurable设置为true ,并将其转换为数据属性:

Object.defineProperty(person, "job", {
    configurable: true,
    enumerable: true,
    get: function() {
        return this._job;
    },
    set: function(value) {
        this._job = value;
    }
})
 
// 设置value和writable其中任意一个即可转换为数据属性        
Object.defineProperty(person, "job", {
    value: 'worker',
    writable: true
})
 
var descriptor = Object.getOwnPropertyDescriptor(person, 'job');
console.log(descriptor);    // {value: "worker", writable: true, enumerable: true, configurable: true
数据描述符value、writable 和访问器描述符get、set不能同时设置,否则会报错。
Object.defineProperties()
定义的属性直接是在对象自身身上并不在原型上
var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
  // etc. etc.
});
Object.defineProperties()
通过Object.defineProperties()可以一次性为对象定义多个属性。

var person = {};
Object.defineProperties(person, {
  name: {
    value: 'Nicy',
    writable: true
  },
  _age: {
    value: 21,
    enumerable: true,
    writable: true,
    configurable: true
  },
   age: {
    get: function() {
    return this._age;
    },
    set: function(value) {
    this._age = value;
    }
  }
});
Object.assign()
1. Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
2. Object.assign方法的第一个参数是目标对象,后面的参数(即可以有多个参数)都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
Object.create()
底层实现:
Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
    };
    
知识借鉴:https://blog.csdn.net/yangyangkl123/article/details/113592132
实例运用
原型式继承而不是原型链继承(因为下面的me的原型式function(){}而不是person)
const person = {
  isHuman: false,
  printIntroduction: function() {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
me.isHuman = true;
Object.entries()
Object.entries()方法返回一个给定对象本身可枚举的属性的键值对数组,其排列与使用for...in 循环遍历该对象时返回的顺序一致(区别在于for-in可循环原型链中设置的属性)。   Object.entries() 是将对象转成一个自身可枚举属性的键值对数组

Object.entries()转换某个对象后的效果键值对数组:
var keyValueArray = [
 ['a', 'something'],
 ['b', 'sometimes']
];
const object1 = {
  a: 'somestring',
  b: 42
};
Object.prototype.name=4
for(let i in object1){
    console.log(i)
}
for (const [key, value] of Object.entries(object1)) {
  console.log(`${key}: ${value}`);
}
Object.freeze()
Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改(也包括属性不能删除);冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改(仅仅是地址)(看清楚是原型,就是prototype,而不是prototype里的属性,里面的属性是可以改的,但prototype想整个替换掉是不行的)。freeze() 返回和传入的参数相同的对象。

"总结:冻结一个对象后,能改的只有prototype里的属性"
const obj = {
  prop: 42
};

Object.prototype.name=4
var o = Object.freeze(obj);
Object.prototype.name=5 // 里面的name被改了
console.log(o)
Object.prototype={a:4} // 改不了
console.log(obj)
Object.fromEntries()
//好像用在Set上会报错
Object.fromEntries() 方法把键值对列表转换为一个对象。
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

const obj = Object.fromEntries(entries);

console.log(obj);
// expected output: Object { foo: "bar", baz: 42 }
Object.getPrototypeOf()
查看某对象是否继承与某对象
const prototype1 = {};

const object1 = Object.create(prototype1);
console.log(object1.__proto__==prototype1)
console.log(Object.getPrototypeOf(object1) === prototype1);

Object原型上的方法

Object.prototype.hasOwnProperty()
hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
const object1 = {};
object1.property1 = 42;

console.log(object1.hasOwnProperty('property1'));
// expected output: true

console.log(object1.hasOwnProperty('toString'));
// expected output: false

console.log(object1.hasOwnProperty('hasOwnProperty'));
// expected output: false

Object注意

1.原本Object.__proto__==Object.prototype,因为会造成无线循环,所以规定Object.prototype.__proto__==null  ,因此现在Object.__proto__ != Object.prototype了

2. Object可以直接当成对象使用,但Object也可以直接访问Object.prototype里的方法的,因为当Object对象自身身上找不到时就会去Object.__proto__上去找,而Object.__proto__执向的是Object.prototype。虽然Object.__proto__不等于Object.prototype,但这只是规定成这样的,实际上是相等的,只是为了让其不死循环才规定其不相等的

数组 Array

数组的概念及操作

1.数组,是有序的元素序列。通俗讲,数组就是有序的数据集合。
2.数组属于对象类型。
3.数组的作用:用于在单个变量中存储多个值。
创建数组
1.字面量的方式
	var arr = [4,2];

2.通过构造函数
	var arr = Array() 和 var arr = new Array() 效果一样
    var arr2 = new Array();
    var arr3 = new Array(3);
    var arr4 = new Array('a','b','c');
    console.log(arr1);
    console.log(arr2);
    console.log(arr3);
    console.log(arr3[0]); // undefined
    console.log(arr3[2]); // undefined
    console.log(arr4);
3.通过数组对象自带方法Array.of()
	var arr = Array.of(1); // [1]
	var arr2 = Array.of(1,2) // [1,2]
    var arr3 = Array.of([1,2]) // [[1,2]]
访问数组元素
数组的长度  arr.length
数组的索引(下标)  arr[0] ~ arr[arr.length-1]
设置值
var arr7 = [];
arr7[0] = 'aa';
arr7[1] = 'bb';
arr7[2] = 'cc';
console.log(arr7);
特性
var arr8 = [];
arr8[4] = 'xxxx';
arr8[7] = 'yyyy';
console.log(arr8); // [undefined,undefined,undefined,undefined,'xxxx',undefined,undefined,'yyyy']
console.log(arr8.length); // 返回数组的长度(数组的元素个数)

数组分类

"对象数组 | 数组对象"
var gp1 = [
  {name:'xw',age:18},
  {name:'xh',age:18},
  {name:'xq',age:18},
  {name:'xj',age:18},
  {name:'xk',age:18},
  {name:'xc',age:18},
  {name:'xv',age:18}
]

var gp2 = {
  name:['xw','xh','xq','xj','xk','xc','xv'],
  age:[12,24,15,16,17,18,19,21,22]
}

var gp3 = [123,'abc',function (){alert('yyyyy')},{}];
gp3[2]();


"多维数组"
var arr11 = [
  'abc',
  123,
  ['a','b','c'],
  {name:'xiaocuo',age:20,fs:['xh','xf']},
  [1,2,['rr','tt']]
]
console.log( arr11[2][2] ); // 'c'
console.log( arr11[4][2][1] ); // 'tt'
console.log(arr11[3].fs[1]); // 'xf'


"稀疏数"
稀疏数组:数组中的某些元素是没有值的,作为概念认识一下就行
 var arr = [3,5,,7,8,,,4] // 稀疏数组
 
 for 循环遍历数组遇到空元素会输出undefined
 for...in在遍历数组的时候会自动跳过空元素

数组常见API

会改变原数组
push()在数组的后面添加(单个或多个)新元素,返回数组的新长度
pop()删除数组的最后一个元素,返回被删除的元素
unshift()在数组的前面添加(单个或多个)新元素,返回数组新的长度
shift()删除数组的第一个元素,返回被删除的元素
splice()可以对数组进行增、删、改,返回一个数组(被删除的元素所组成的数组)
    删除n项:arr.splice(起始位置,n)    注:起始位置是下标
    增加元素:arr.splice(起始位置,0,添加1,添加2.....)
    修改元素:arr.splice(起始位置,2,替换1,替换2.....)
sort() 对数组的元素进行排序,返回数组
	arr.sort() "默认按照字符编码排序,有一个缺陷就是只要第一位分出顺序了,就不会看第二位"
reverse() 颠倒数组中元素的顺序。返回数组
	arr.reverse();

以上方法都会改变原数组!
"push"
var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.push(55,66);
console.log(arr); // [11, "aa", "bb", 33, 78, "abc", 55, 66]
console.log(results); // 8

"pop"
var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.pop();
console.log(arr); // [11, 'aa', 'bb', 33, 78]
console.log(results); // 'abc'

'unshift'
var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.unshift('yoyo');
console.log(arr); // ['yoyo', 11, 'aa', 'bb', 33, 78, 'abc']
console.log(results) // 7

'shift'
var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.shift();
console.log(arr); // ['aa', 'bb', 33, 78, 'abc']
console.log(results); // 11

'splice'
var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.splice(3,2); // 删除元素
console.log(arr); // [11, 'aa', 'bb', 'abc']
console.log(results); // [33,78]

var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.splice(2,0,'cc','dd'); // 新增元素
console.log(arr); // [11, 'aa', 'cc', 'dd', 'bb', 33, 78, 'abc'];
console.log(results); // []

var arr = [11, 'aa', 'bb', 33, 78, 'abc'];
var results = arr.splice(4,1,77); // 替换元素
console.log(arr); // [11, 'aa', 'bb', 33, 77, 'abc'];
console.log(results); // [78]

'reverse'
var arr = [1,2,3,4,5];
var results = arr.reverse();
console.log(arr); // [5,4,3,2,1]
console.log(results); // [5,4,3,2,1]
console.log(arr === results) // true

'sort'
1.默认的sort是按照编码来排序,只要第一位分出顺序了,就不会看第二位
var arr = [5,24,6,27,33,1,24]
console.log(arr.sort()) // [1, 24, 24, 27, 33, 5, 6]

2.指定排序规则,这种会看所有的位置
var arr = [5,24,6,27,33,1,24]
var results = arr.sort(function (a, b) {
    // 如果这里的返回值大于0,那么a和b就会交换顺序,不大于0就不交换
    return a - b // 升序
    // return b - a // 降序
})
console.log(arr) // [1, 5, 6, 24, 24, 27, 33]
console.log(arr === results) // true

注意:以上这种仅仅是会对数字进行第二位,第三位...等的排序,并不会对字母进行第二位,第三位的排序
	如: var arr = [5,24,6,27,33,1,24,111,110,112,'ac','ae','ab','ad','aa','aac','aaa']
    	// [1, 5, 6, 24, 24, 27, 33, 110, 111, 112, 'ac', 'ae', 'ab', 'ad', 'aa', 'aac', 'aaa']
不会改变原数组
slice()从数组中拷贝一部分,返回新数组
	arr.slice(1,4)包含索引为1的元素,不包含索引为4的元素(拷贝索引为123的元素)
concat()用于合并数组,返回新数组
	arr.concat(arr1,arr2....)
toString()将数组转成字符串,返回字符串
join()将数组转成字符串,参数为分隔符,返回字符串

以上方法不会改变原数组!
'slice'
var arr3 = ['a', 'b', 'c', 'd', 'e', 'f'];
var results = arr3.slice(1,);
console.log(arr3); // ['a', 'b', 'c', 'd', 'e', 'f']
console.log(results); // ['b', 'c', 'd', 'e', 'f']

'concat'
var arr1 = ['a', 'b', 'c'];
var arr2 = [1,2,3];
var results = arr1.concat(arr2);
console.log(arr1) // ['a', 'b', 'c']
console.log(results); // ["a", "b", "c", 1, 2, 3]

'toString'
var arr = ['a', 'b', 'c'];
var results = arr.toString();
console.log(arr) // ['a', 'b', 'c']
console.log(results); // 'a,b,c'

'join'
var arr = ['a', 'b', 'c'];
var res = arr.join();
var res2 = arr.join('-')
console.log(arr) // ['a', 'b', 'c']
console.log(res) // 'a,b,c'
console.log(res2) // 'a-b-c'
ES5新增的数组方法
2个索引方法:indexOf()和lastIndexOf();
5个迭代方法:forEach()、map()、filter()、some()、every();
2个归并方法:reduce()、reduceRight();
1.索引方法
	'indexOf'
    var arr = [4,2,2,4,6]
    // 从arr当中去找2的索引,如果数组里有多个2,得到的是第一次出现的索引
    console.log(arr.indexOf(2)) // 1
    console.log(arr.indexOf(10)) // -1 如果值不存在,那么得到-1

	'lastIndexOf'
    // 判断数组里是否存在某个值,就用indexOf判断结果是否等于-1
    // 从arr当中去找2的索引,如果数组里有多个2,得到的是最后一次出现的索引
    console.log(arr.lastIndexOf(2))
    console.log(arr.lastIndexOf(10)) // -1 如果值不存在,那么得到-1
  

2.迭代方法
	"forEach"
    // 自动跳过空元素
    // forEach的时候传递一个函数进来,我们只需要把他写进来,不需要调用
    // 这个函数就会被forEach去调用
    // arr有多少个元素,函数就会被调用多少遍
    // 每一次调用的时候都会传递两个实参过来
	var arr = [5,32,5,7,2,3]
    arr.forEach(function (item, index) {
        console.log(item, index)
        //无论return 什么均为undefined
    })


	'map'
    // map也是用来遍历数组,遍历的同时需要给一个返回值
    // 每一次遍历的时候返回一个值,这些值会放进一个新的数组里
    var arr = [5,32,5,7,2,3]
    var arr1 = arr.map(function (item ,index) {
        console.log(item, index)
        return item*2
    })
    console.log(arr1)

	'filter'
    // 过滤
    // 每一次遍历的时候都要返回一个条件(布尔值)
    // 当这个条件得结果为true的时候,当前值就会被保留,否则就被过滤掉
    // 最终用一个新的数组来存结果
    // 如果不写return,默认返回值是undefined,会隐式转换为false,相当于每次都返回的false
    var arr = [5,7,3,7,3,2]
    var arr1 = arr.filter(function (item, index) {
        console.log(item, index)
        return item > 4
    })
    console.log(arr1)

	'some'
    // some的最终结果是一个布尔值
    // 每一次遍历的时候返回一个条件
    // 只要在遍历过程中有一条数据满足条件,整个some的结果就是true
    // 在遍历的过程中只要发现了一个true就停止遍历
    var arr = [5,2,5,2,6,7]
    var is = arr.some(function (item, index) {
        console.log(item, index)
        return item > 10
    })
    console.log(is)

	'every'
    // every 必须每一次遍历返回的条件都是true,结果才为true,否则就是false
    // 只要任意一个不满足条件,遍历就结束,最终得到false
    var arr = [5,2,5,2,6,7]
    var is = arr.every(function (item, index) {
        console.log(item, index)
        return item % 2 === 0
    })
    console.log(is)


3.归并方法
	'reduce'
    var arr = [3,5,2,6,2]
    // 第一次的res是arr[0], 以后的res是上一次的返回值
    var sum = arr.reduce(function (res, curr, index) {  // 计算arr所有元素的累加和
        // index是curr的索引, 默认从1开始, 但是一般用的不多
        console.log(res, curr, index)
        return res + curr
    })
    console.log(sum)

    // reduce有两个参数, 第一个参数是函数, 第二个参数可选的默认值
    // 在后面写上一个默认值, 循环次数就跟数组长度相等, res默认值就是1, curr从arr[0]开始
    var arr = [3,5,2,6,2]
    var sum = arr.reduce(function (res, curr, index) {
        console.log(res, curr, index)
        return res *curr
    }, 1)
    console.log(sum)

	'reduceRight'
    // reduce是从左往右, reduceRight从右往左
    // reduceRight的语法和reduce是完全一样的
    var arr = [3,5,2,6,2]
    var sum = arr.reduceRight(function (res, curr) {
        return res.concat(curr)
    }, [])
    console.log(sum)

清空数组

var arr = [3,4,5,6,7];
console.log(arr);

arr.length = 0; // 建议使用这种方法(直接将数组内的东西扔掉)
console.log(arr);

遍历数组

1. for循环
2. for in遍历
3. for of遍历
4. forEach遍历

'for in 遍历'
var arr=["张三","李四","王五","赵六"];
for (var i in arr){
	console.log(i,":",arr[i]);
}

'for of遍历'
var arr=["张三","李四","王五","赵六"];
for (var value of arr){
    console.log(value);
}


'forEach遍历'
var arr=[1,2,3,4];
arr.forEach(function(val, index) {
	console.log(val, index);
});

`for循环和for of遍历数组遇到空元素会输出undefined`
`for...in在遍历数组的时候会自动跳过空元素`

数组去重

"方法一"
	// 利用选择排序法  数字和字符组合也可去重
    function uniq(array){
        var temp = {}, r = [], len = array.length, val, type;
        for (var i = 0; i < len; i++) {
            val = array[i];
            type = typeof val;
            if (!temp[val]) {
                temp[val] = [type];
                r.push(val);
            } else if (temp[val].indexOf(type) < 0) {
                temp[val].push(type);
                r.push(val);
            }
        }
        return r;
    }
    var aa = [1,2,"2",4,9,"a","a",2,3,5,6,5];
    console.log(uniq(aa));


"方法二"
	// 仅仅纯数字可以去重
    var a = [1,1,1,22,2,3,5,8,];
    var obj={};
    var arr1 = [];
    for(var i=0,len=a.length;i<len;i++){
        if(!obj[a[i]]){ 

            obj[a[i]]='abc';
            arr1.push(item);
        }
    }
    console.log(obj)

"方法三"
	// 利用Set数据类型的默认不允许重复进行去重
    function combine(){
        let arr = [].concat.apply([], arguments);  //没有去重复的新数组
        return Array.from(new Set(arr));
    }

    var m = [1, 2, 2], n = [2,3,3];
    console.log(combine(m,n));  

数组排序

一下排序方式均是只适用于 纯数字 数组
冒泡排序
var arr = [4,2,5,7,8,2,1]
for (var i = 0; i < arr.length - 1; i++) {
    for (var j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j+1]) {
            var temp = arr[j]
            arr[j] = arr[j+1]
            arr[j+1] = temp
        }
    }
}
console.log(arr)
选择排序
var arr = [1,2,3,4]
for (var i = 0; i < arr.length - 1; i++) {
    var min = i    
    for (var j = i + 1; j < arr.length; j++) { 
        if (arr[min] > arr[j]) {
            min = j
        }
    }
    if (i != min) {
        var temp = arr[i]
        arr[i] = arr[min]
        arr[min] = temp
    }
}
快速排序
// 快速排序1 这种是去重排序
function quickSort(arr){
    if (arr.length <= 1) {//出口条件
        return arr;
    }
    // 找中点
    var midIndex = parseInt(arr.length/2);//中点下标
    var mid = arr[midIndex];//中点对应的值
    // 分左右
    var left = [];
    var right = [];

    for (var i = 0; i < arr.length; i++){
        if (mid == arr[i]) {
            continue;
        }
        if (mid > arr[i]) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort(left).concat([mid],quickSort(right));
}
var arr = [4,2,5,1,6,8,10,7];
console.log( quickSort(arr) );


// 快速排序2  这种事不去重排序

var arr = Array(20);
for (var k = 0; k < arr.length; k++) {
    arr[k] = Math.round(Math.random() * 20)
}
console.log(arr)
function kuaiSu(arr) {
    if (arr.length <= 1)
        return arr
    var left = [], right = [], mid = [];
    var midIndex = parseInt(arr.length / 2);
    mid[0] = arr[midIndex]

    var n = 0; 
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] == mid[0]) {

            n++;
            if (n >= 2)
            { mid.push(arr[i]); }
            continue;
        }
        if (arr[i] < mid[0]) { left.push(arr[i]) }
        else { right.push(arr[i]) }

    }
    return kuaiSu(left).concat(mid, kuaiSu(right))

}
console.log(kuaiSu(arr))
桶排序
// 这里的只适合不重复的数
// 桶排序:创建桶,把数据放到桶里(对应下标的位置),遍历桶取出有数据的桶(的下标)......
function bucketSort(arr){
    var bucket = [];
    for (var i = 0; i < arr.length; i++){
        var item = arr[i];
        bucket[item] = 'abc';
    }
    arr.length = 0;
    for (var index in bucket){
        arr.push(parseInt(index));
    }
    return arr;
}

Number

二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
    0b111110111 === 503 // true
    0o767 === 503 // true

从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。
    // 非严格模式
    (function(){
      console.log(0o11 === 011);
    })() // true

    // 严格模式
    (function(){
      'use strict';
      console.log(0o11 === 011);
    })() // Uncaught SyntaxError: Octal literals are not allowed in strict mode.

如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。
Number('0b111')  // 7
Number('0o10')  // 8

数值分隔符

'分隔符(_)'
欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,1000可以写作1,000。ES2021,允许 JavaScript 的数值使用下划线(_)作为分隔符。	
    let budget = 1_000_000_000_000;
    budget === 10 ** 12 // true

这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。
    123_00 === 12_300 // true
    12345_00 === 123_4500 // true
    12345_00 === 1_234_500 // true
    小数和科学计数法也可以使用数值分隔符。
    0.000_001 // 小数
    1e10_000 // 科学计数法

'数值分隔符有几个使用注意点'
    1.不能放在数值的最前面(leading)或最后面(trailing)。
    2.不能两个或两个以上的分隔符连在一起。
    3.小数点的前后不能有分隔符。
    4.科学计数法里面,表示指数的e或E前后不能有分隔符。
	下面的写法都会报错。
        // 全部报错
        3_.141
        3._141
        1_e12
        1e_12
        123__456
        _1464301
        1464301_

除了十进制,其他进制的数值也可以使用分隔符。
    // 二进制
    0b1010_0001_1000_0101
    // 十六进制
    0xA0_B0_C0
    可以看到,数值分隔符可以按字节顺序分隔数值,这在操作二进制位时,非常有用。

注意,分隔符不能紧跟着进制的前缀0b、0B、0o、0O、0x、0X。
    // 报错
    0_b111111000
    0b_111111000

数值分隔符只是一种书写便利,对于 JavaScript 内部数值的存储和输出,并没有影响。
    let num = 12_345;
    num // 12345
    num.toString() // 12345
    上面示例中,变量num的值为12_345,但是内部存储和输出的时候,都不会有数值分隔符。

下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是语言的设计者认为,'数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。'
    Number()
    parseInt()
    parseFloat()
    Number('123_456') // NaN
    parseInt('123_456') // 123

总结:'数值分隔符可用于各种进制,并且对计算无影响知识为了更好书写和阅读;有几个注意事项需要注意'

数值和全局方法

'Number.isFinite' Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。如果参数类型不是数值,Number.isFinite一律返回false
'Number.isNaN' 检查一个值是否为NaN,如果参数类型不是NaN,Number.isNaN一律返回false

它们与传统的全局方法isFinite()和isNaN()的区别在于,'传统方法先调用Number()将非数值的值转为数值,再进行判断',而'这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false'

————————————————————————————————————————————————分隔符

ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
    // ES5的写法
    parseInt('12.34') // 12
    parseFloat('123.45#') // 123.45

    // ES6的写法
    Number.parseInt('12.34') // 12
    Number.parseFloat('123.45#') // 123.45
    这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

    Number.parseInt === parseInt // true
    Number.parseFloat === parseFloat // true

String

charAt()	返回在指定位置的字符。
indexOf()	返回某个指定的字符串值在字符串中首次出现的位置。
lastIndexOf()	从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置。
charCodeAt()	返回在指定的位置的字符的 Unicode 编码。
fromCharCode()	将 Unicode 编码转为字符。
concat()	连接两个或更多字符串,并返回新的字符串。
match()	查找找到一个或多个正则表达式的匹配。
replace()	在字符串中查找匹配的子串,并替换与正则表达式匹配的子串。
replaceAll()	在字符串中查找匹配的子串,并替换与正则表达式匹配的所有子串。
search()	查找与正则表达式相匹配的值。
slice()	提取字符串的片断,并在新的字符串中返回被提取的部分。
split()	把字符串分割为字符串数组。
substring()	提取字符串中两个指定的索引号之间的字符。
toLowerCase()	把字符串转换为小写。
toUpperCase()	把字符串转换为大写。
toLocaleLowerCase()	根据本地主机的语言环境把字符串转换为小写。
toLocaleUpperCase()	根据本地主机的语言环境把字符串转换为大写。
trim()	去除字符串两边的空白。 (ES5)
startsWith()	查看字符串是否以指定的子字符串开头。 (ES6)
endsWith()	判断当前字符串是否是以指定的子字符串结尾的(区分大小写)。 (ES6)
includes()	查找字符串中是否包含指定的子字符串。(ES6)
repeat()	复制字符串指定次数,并将它们连接在一起返回。 (ES6)
valueOf()	返回某个字符串对象的原始值。
toString()	返回一个字符串。

https://blog.csdn.net/r1342476713/article/details/100113024
https://blog.csdn.net/qq_35087256/article/details/80188779
上方可以解释at charAt charCodeAt codePointAt的区别

"注意"
substring()方法会把第二个参数转换为0,使调用变成了substring(3,0),而由于这个方法会将较小的数作为开始位置,将较大的数作为结束位置,因此最终相当于调用了 substring(0,3) 。

slice()方法会将传入的负值与字符串的长度相加;
substring()方法会把所有的负值参数都转换为0;

知识借鉴:https://blog.51cto.com/u_15740575/5545664

Date

日期对象创建
   时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数(时间戳)

var date = new Date(); // 当前时间的日期对象
var data = new Date(2012,6,10); // Tue Jul 10 2012 00:00:00 GMT+0800 (中国标准时间)
Date.now() // 获取当前时间戳,得到一个number

API

"get系列"
    getFullYear()  返回年                                           
    getMonth()     返回月份 0 - 11                                    
    getDate()      返回某一天                                       
    getDay()       返回星期 0 - 6                                      
    getHours()     返回小时 0 - 23                                      
    getMinutes()   返回分钟 0 - 59                                        
    getSeconds()   返回秒   0 - 59                                        
    getMilliseconds() 返回毫秒 0-999
    getTime()      返回1970年1月1日午夜到指定日期(字符串)的毫秒数 

    var d = new Date();
    console.log(d); // Tue Aug 13 2019 09:31:03 GMT+0800 (中国标准时间)
    console.log( d.getFullYear() );
    console.log( d.getMonth() );
    console.log( d.getDate() );
    console.log( d.getHours() );
    console.log( d.getUTCHours() ); // UTC世界时
    console.log( d.getMinutes() );
    console.log( d.getSeconds() );
    console.log( d.getMilliseconds() );
    console.log( d.getDay() );



'set系列'
    setFullYear()  设置年份            
    setMonth()     设置月 0 - 11  	                  
    setDate()      设置天                     
    setHours()     设置小时 0 - 23                
    setMinutes()   设置分钟 0- 59            
    setSeconds()   设置秒   0 - 59                  
    setTime()      使用毫秒的形式设置时间对象 
    
    // 可以把日期拨到某个时间点
    date.setFullYear(2021) // 把date日期对象设置为2021年
    date.setMonth(5) // 把日期拨到6月
    date.setDate(20) // 把日期设置成20号
    date.setHours(7)
    date.setMinutes(7)
    date.setSeconds(7)
    date.setTime(0) // 设置时间戳,date就会根据时间戳来计算,设置为0那么date九变成了1970-1-1,这个方法一般用
    的不多
    date.setFullYear(2012,3,5) 设置日期为2012年4月5号
    date.setHours(13,30,0) 设置时间为13:30:00
	
	'month'
	可选,介于 0 ~ 11 之间:如果不填,取系统当月
    -1 为去年的最后一个月
    12 为明年的第一个月
    13 为明年的第二个月
    
    'data'
	可选, 表示月中某一天的数值。如果不填,取系统当日
    用本地时间表示。介于 1 ~ 31 之间:
    0 为上个月最后一天
    -1 为上个月最后一天之前的天数
    如果当月有31天:32 为下个月的第一天
    如果当月有30天:
    32 为下一个月的第二天
toString
 <script>
    var date = new Date()
    console.log(date)
    console.log(date.toString())
    console.log(date.toDateString()) // 转换为日期字符串
    console.log(date.toLocaleDateString()) // 转换为本地日期字符串
    
    console.log(date.toTimeString()) // 转换为时间字符串
    console.log(date.toLocaleTimeString()) // 转换为本地时间字符串
    
    console.log(date.toLocaleString()) // 转换为本地日期加时间完整的字符串  (常用)
    console.log(date.toUTCString()) // 转换为标准时区的日期和时间
  </script>

image-20231129133335299

Math

'对象属性'
    Math.E         返回算术常量 e,即自然对数的底数(约等于2.718)。 
    Math.LN2       返回 2 的自然对数(约等于0.693)。                
    Math.LN10      返回 10 的自然对数(约等于2.302)。               
    Math.LOG2E     返回以 2 为底的 e 的对数(约等于 1.414)。        
    Math.LOG10E    返回以 10 为底的 e 的对数(约等于0.434)。        
    Math.PI        返回圆周率(约等于3.14159)。                     
    Math.SQRT1_2   返回返回 2 的平方根的倒数(约等于 0.707)。       
    SQRT1_2.SQRT2  返回 2 的平方根(约等于 1.414)                   
    "Math对象上的属性都是常量,不是变量,不能被修改"


'对象方法'
    abs(x)    返回数的绝对值。                         
    sin(x)    返回数的正弦。                           
    cos(x)    返回数的余弦。                           
    tan(x)    返回角的正切。                           
    ceil(x)   对数进行上舍入。                         
    floor(x)  对数进行下舍入。                         
    round(x)  把数四舍五入为最接近的整数。             
    max(x,y)  返回 x 和 y 中的最高值。(可传多个数值) 
    min(x,y)  返回 x 和 y 中的最低值。(可传多个数值) 
    pow(x,y)  返回 x 的 y 次幂。                       
    sqrt(x)   返回数的算术平方根。                     
    random()  返回 0 ~ 1 之间的随机小数,包含0不包含1  

Error

var a = -20
if (a > 0) {
    // 正常逻辑代码
} else {
    // 本身代码不会报错,我们制造报错
    console.error(new Error('a不大于0,我给你报个错'))
}

BOM

Browser Object Model:浏览器对象模型

BOM的概念及作用

1.BOM提供了独立于内容而与浏览器窗口进行交互的对象,核心对象是window
2. JavaScript语法的标准化组织是ECMA,DOM的标准化组织是W3C,BOM缺乏标准,BOM最初是Netscape浏览器标准的一部分
3. DOM是为了操作文档节点出现的API,document是其的一个对象,BOM是为了操作浏览器对象出现的API,window是其的一个对象

原生对象与宿主对象

1. native object: 原生对像  就是ECMA所定义的对象,如Number、String、Boolean、Object、Array、Function、Date、RegExp、内置对象(如 Math、Global不需要实例化)、Error .....
2. host object: 宿主对象 如window、DOM、BOM

window对象

window上属性有很多,这里只展示常用的几个   "window对象是浏览器中的Global(全局)对象"

'window.open()'
    var newWindow = window.open(URL,name,specs);  返回一个新窗口
         URL:打开页面的URL,没有指定URL将打开新的空白窗口
              (必须为全称,不能是www.baidu.com,而要是http://www.baidu.com)
         name:_blank  新窗口打开,默认
               _self  当前页面打开
               ......
         specs:一个逗号分隔的项目列表。支持以下值:
                width=pixels    height=pixels  最小值为100
                left=pixels        top=pixels    ......
                示例 window.open('','','width=200,height=200');
    function openWin(){
        myWindow=window.open("","","width=200,height=100");
        myWindow.document.write("<p>这是'我的窗口'</p>");
    }
    function closeWin(){
        myWindow.close(); // 方法用于关闭浏览器窗口(新打开的)
    }


'resizeTo()、resizeBy()'
    调整窗口大小  objW.resizeTo(width,height);设置窗口的宽度(其中width和height为必须)
    调整窗口大小 objW.resizeBy(width,height);要使窗口宽度增加的像素数(其中width为必须,height为可选)
    var objW=window.open('','', 'width=100,height=100');
    objW.resizeTo(500,500);

    注:此功能在一些标签型浏览器中无效。

'setInterval()、setTimeout()'
    window.setInterval ( 函数/名称 , 毫秒数 )
    表示每经过一定的毫秒后,执行一次相应的函数(重复)
    window.setTimeout ( 函数/名称 , 毫秒数 )
    表示经过一定的毫秒后,只执行一次相应的函数(不重复)
    清除计时器:clearInterval( ); 
			  clearTimeout( );

'onbeforeunload'
    window.onbeforeunload = function(e){ // 离开当前页面触发事件
        var e = window.event || e; // 兼容IE8和Firefox 4之前的版本
        if(e){e.returnValue = '关闭提示';}
        return '关闭提示'; // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
    }

'onscroll'
// 滚动条滚轮事件(滚动触发事件) 注意:onscroll没有滚动条的时候是不会触发的
// onscroll也可以用在其他元素上
window.onscroll = function () {
    // window.scrollTop有兼容性,直接取取不到
    // 下面这句话就是获取滚动条高度的一个兼容写法
    var top = document.documentElement.scrollTop || document.body.scrollTop
    var left = document.documentElement.scrollLeft || document.body.scrollLeft

    console.log(top, left)
}


'onresize'
// 窗口缩放事件
window.onresize = function () {
    // 当窗口大小发生改变的时候触发的事件
    console.log(window.innerWidth, window.innerHeight)
}

history对象

history对象包含有关用户的访问历史记录

length 返回浏览器历史列表中的 URL 数量
forward() 加载 history 列表中的下一个 URL
back() 加载 history 列表中的上一个 URL
go() 加载 history 列表中的某个具体页面
history.go(-1)    后退一页
history.go(1)    前进一页

练习:
由页面a跳转到页面b,5秒后自动返回页面a,也可点击立即返回

location对象

- window.location.href         当前页面的 URL,可以获取,可以修改(页面跳转)。
- window.location.hostname   web 主机的域名
- window.location.pathname   当前页面的路径和文件名
- window.location.port        web 主机的端口 (80 或 443)
- window.location.protocol     所使用的 web 协议(http:// 或 https://)
- window.location.search      请求参数(?后面的内容)
- window.location.reload()    方法重新加载当前文档(刷新)
- window.location.replace()   方法用新的文档替换当前文档
navigator.userAgent  返回浏览器信息(可用此属性判断当前浏览器)

判断当前浏览器类型的代码:
function isBrowser() {
   var userAgent = navigator.userAgent;
   //微信内置浏览器
   if(userAgent.match(/MicroMessenger/i) == 'MicroMessenger') {
     return "MicroMessenger";
   }
   //QQ内置浏览器
   else if(userAgent.match(/QQ/i) == 'QQ') {
     return "QQ";
   }
   //Chrome
   else if(userAgent.match(/Chrome/i) == 'Chrome') {
     return "Chrome";
   }
   //Opera
   else if(userAgent.match(/Opera/i) == 'Opera') {
     return "Opera";
   }
   //Firefox
   else if(userAgent.match(/Firefox/i) == 'Firefox') {
     return "Firefox";
   }
   //Safari
   else if(userAgent.match(/Safari/i) == 'Safari') {
     return "Safari";
   }
   //IE
   else if(!!window.ActiveXObject || "ActiveXObject" in window) {
    return "IE";
   }
   else {
     return "未定义:"+userAgent;
   }
}

screen对象

screen对象包含有关客户端显示屏幕的信息

width 属性返回显示器屏幕的宽度
height 属性返回显示器屏幕的高度
availHeight 属性返回显示屏幕的高度 (除 Windows 任务栏之外)
availWidth 属性返回显示屏幕的宽度 (除 Windows 任务栏之外)

总结

window常用属性
setInterval setTimeout xx.clearInterval xx.clearTimeout
alert confirm prompt 
window.onbeforeunload
window.onload "页面加载完成以后才执行"

window.open(URL,name,specs)
objW.close()
objW.resizeTo(width,height) 均为必选
objW.resizeBy(width,height) width为必选,height为可选


浏览器相对屏幕坐标(获取)
window.screenLeft
window.screenTop
window.screenX
window.screenY

浏览器窗口文档显示区域(获取 不包含滚动条)
window.innerWidth
window.innerHeight
(获取包含滚动条)
window.outerWidth
window.outerHeight

获取浏览器窗口文档显示区域滚动的距离 (获取)
window.pageXOffset
window.pageYOffset

// 浏览器窗口文档显示区域滚动的距离 (获取或设置)
文档对象获取
高度方向:document.documentElement.scrollTop || document.body.scrollTop,
宽度方向:document.documentElement.scrollLeft || document.body.scrollLeft

// 设置浏览器窗口文档显示区域滚动的距离 (设置)
window.scrollBy(x-coord,y-coord)	按照指定的像素值来滚动内容。(在原有的基础上)
window.scrollTo(x-coord,y-coord)	把内容滚动到指定的坐标。

可应用于其他元素 scrollTop、scrollLeft、scroll(和scrollTo方法一样)、scrollBy

location:
window.location.href
window.location.hostname
window.location.pathname
window.location.port
window.location.protocol
window.location.search
window.location.reload
window.location.replace

history:
history.go(1)
history.back()
history.forward()

screen:
window.screen.width 返回当前屏幕宽度(分辨率值)
window.screen.height 返回当前屏幕高度(分辨率值)
availHeight 属性返回显示屏幕的高度 (除 Windows 任务栏之外)
availWidth 属性返回显示屏幕的宽度 (除 Windows 任务栏之外)

navigator:
navigator.userAgent  返回浏览器信息(可用此属性判断当前浏览器)

DOM

DOM的概念及作用

Document Object Model -- DOM(文档对象模型),DOM是针对HTML和XML文档的一个API。

1. DOM描绘了一个层次化的节点树,即HTML文档中的所有内容都是节点(node)。
2. DOM树中的所有节点均可通过JS进行访问,允许开发人员添加、移除、修改和查询页面的某一部分。

节点类型

整个文档是一个文档节点
每个HTML元素是元素节点
HTML元素内的文本是文本节点(回车也是文本节点)
每个HTML的属性是属性节点
注释是注释节点

nodeType 属性:返回一个整数,这个数值代表节点的类型
元素节点  返回 1      
属性节点  返回 2      
文本节点  返回 3       
注释节点  返回 8      
文档节点  返回 9

nodeName 属性:返回节点的名称
元素节点的 nodeName 是标签名称   ( 大写 )
属性节点的 nodeName 是属性名称
文本节点的 nodeName 永远是 #text
注释节点的 nodeName 永远是 #comment
文档节点的 nodeName 永远是 #document

tagName 属性:返回元素的标签名。
在 HTML 中,tagName 属性的返回值始终是大写的。

nodeValue 属性:返回节点的值
对于元素节点,nodeValue 返回值是 null
对于文本节点,nodeValue 返回文本内容
对于属性节点,nodeValue 返回属性值
对于注释节点,nodeValue 返回注释内容
对于文档节点,nodeValue 返回 null

对于元素节点,用 innerHTML/innerText / value 设置或取值

节点获取方法:https://juejin.cn/post/7185470457527664701

节点关系

1.父(parent)、子(child)和同胞(sibling)等术语用于描述这些关系。
2.父节点拥有子节点,同级的子节点被称为同胞(兄弟或姐妹)。

    childNodes 获取当前元素节点的所有子节点
    firstChild 获取当前元素节点的第一个子节点
    lastChild 获取当前元素节点的最后一个子节点
    previousSibling 获取当前节点的前一个同级节点
    nextSibling 获取当前节点的后一个同级节点

    ****以上五中方法都包含空白文本节点(不常用,一般用下面的)

    firstElementChild  获取当前元素节点的第一个元素子节点
    lastElementChild 获取当前元素节点的最后一个元素子节点
    nextElementSibling//下一个兄弟元素
    previousElementSibling//上一个兄弟元素
    ownerDocument 获取该节点的文档根节点,相当与 document
    parentNode 获取当前节点的父元素
    children  所有元素子节点

获取元素、元素的值

"获取元素有两种方式"

不常用:
1. document.getElementById(id名)
2. document.getElementsByTagName(标签名)     得到的是一个集合(不止一个,是一堆)
3. document.getElementsByName( ) 通过Name属性值获取元素,返回值是集合,通常用来获取有name的input的值;
4. document.getElementsByClassName(class名称)     但是:IE8以下不能用
5. children属性,获得DOM元素的所有子元素;返回值是集合
6. parentNode属性,获得DOM元素的父级元素

常用:
7、ES5选择器:
    document.querySelector ()   一旦匹配成功一个元素,就不往后匹配了   
    document.querySelectorAll ()  匹配到所有满足的元素, 支持IE8+

——————————————————————————————————————————————————————

"获取元素的值"

表单元素
	元素.value

非表单元素
    元素.innerHTML
    元素.innerText

元素样式
    元素.style.样式='值'

获取元素

<button class="btn">按钮1</button>
<button class="btn">按钮2</button>
<button class="btn">按钮3</button>
<input type="text" name="user">
<div id="box">一个块、。。<span>一个span标签</span></div>
<select id="sel">
    <option value="1">男</option>
    <option value="0">女</option>
    <option value="2">伪娘</option>
</select>
<script>
    获取元素
    var oBox = document.getElementById('box');//返回单个元素
    var aIpt = document.getElementsByTagName('input');//返回的是元素集合(伪数组)
    var inp = document.getElementsByName('user');//返回的是元素集合
    var inp = document.getElementsByName('user')[1];(获取元素集合里的第一个)

    IE 6 7 8不支持
    var aBtn = document.getElementsByClassName('btn');//返回的是元素集合(伪数组,类数组对象)
    var aBtn = document.getElementsByClassName('btn');
    console.log(oBox);
    console.log(aBtn);
    //aBtn.style.color = 'blue';(不行)
    aBtn[1].style.color = 'red';(第二个元素设定颜色)


    var aBtn = document.getElementsByClassName('btn')[1];(直接获取第二个元素)
    aBtn.style.color = 'blue';(aBtn里面就只存储一个,因为只获取了一个)


    IE 6 7 8不支持
    var oBox = document.querySelector('#box');//返回单个元素(返回匹配到的第一个元素) 查询选择器
    var aBtn = document.querySelectorAll('.btn');//返回的是元素集合
    var aBtn = document.querySelector('.btn');////返回单个元素
    var aIpt = document.querySelectorAll('input');//返回的是元素集合
    console.log(oBox);
    console.log(aBtn);
    console.log(aIpt);
    aIpt[0].style.borderColor = 'red';

获取元素的值

1.获取非表单元素的值,也可以设置( div p h1 span ...)
innerHTML:设置或返回元素的文本内容和标签
innerText:设置或返回元素的文本内容(不包含标签)

console.log( oBox.innerHTML ); // 获取
oBox.innerHTML = '修改过后的内容'; // 设置
oBox.innerHTML = '<h1>一个标题</h1><p>一段内容</p>'; // 设置

console.log( oBox.innerText ); // 获取
oBox.innerText = '<h1>一个标题</h1><p>一段内容</p>'; // 设置


2.获取表单元素的值,也可以设置:value(获取表单元素的值不能用innerText和innerHTML)
aBtn.onclick = function (){
    console.log( aIpt[0].value ); // 获取
    aIpt[0].value = '请输入。。。';
    console.log( sel.value ); // 获取下拉列表的值
    sel.style.width = '200px'; // 元素样式
    sel.style.borderColor = 'pink'; // 元素样式
    sel.className = 'red'; // 直接在html里标签内添加class="red",前提是已经有设置这个类选择器
}

元素属性

获取和设置

属性获取和设置
1. getAttribute( )获取元素的属性值,他是节点的方法!所以前缀必须是节点!
     document.getElementById( ID值 ).getAttribute( )
     什么是元素属性呢? class就是元素属性,写在标签内的所有东西都是标签属性, 比如link的href比如img的src....都是元素属性。
    元素自带的属性可以直接用 . 语法获取,但是自定义属性需要 getAttribute() 和 setAttribute( ) 方法
2. setAttribute( )设置元素的属性。同上;
     有些小小的兼容性问题,低版本IE不兼容;
     设置的属性永远都是字符串类型
3. removeAttribute( )删除属性;同上;
     兼容性问题同上;



var oBox = document.getElementById('box');
console.log(oBox.attributes)                   // 获取所有,该节点的属性信息;
console.log(oBox.attributes.length);           // 返回属性节点个数
console.log(oBox.attributes[0]);               // 返回第一个属性节点
console.log(oBox.attributes[0].nodeType);      //  属性
console.log(oBox.attributes[0].nodeValue);     // 属性值
console.log(oBox.attributes['id']);             // 返回属性为 id 的节点 
console.log(oBox.attributes.getNamedItem('id')); // 获取 id 的节点;

attributes属性一般只用作获取,设置使用setAttribute()
 <ul>
    <li index="1">首页</li>
    <li index="2" class="ac">列表</li>
    <li index="3">详情</li>
    <li index="4">关于</li>
  </ul>
  <script>
    var ac = document.querySelector('.ac')
    // 这样是取不到的,因为index是自定义的而不是标准里的
    // console.log(ac.index)
    // getAttribute用来获取自定义属性
    var index = ac.getAttribute('index')
    console.log(index-1)

    // 设置自定义属性
    // 自定义属性的名称可以随便写,但是w3c推荐用 data- 
    // 自定义属性设置无论什么类型的值,最后获取统一都是字符串
    ac.setAttribute('data-id', true)

    // 删除自定义属性,属性名和属性值都没了
    ac.removeAttribute('index')

  </script>

设置class属性值

<button onclick="fn()">按钮</button>
<div id="box" class="box section">box</div>
<script>
    var div = document.querySelector('#box')
    function fn () {
        div.id = 'box1'
        div.className = 'box2' // 这种写法是box2把原来的box直接覆盖
        div.className = 'box box2'
        div.className += ' box2'

        // classList仅支持IE8+

        // 添加一个class
        div.classList.add('ac')

        // 移出一个class,如果要移出多个,那就分两次写
        div.classList.remove('section')

        console.log(div.className)
        console.log(div.classList)
        console.log(div.classList[0])
        console.log(div.classList[1])
    }
</script>

获取style属性值

.box {
    width: 200px;
    height: 300px;
    background: red;
}

<div class="box"></div>

var box = document.querySelector('.box')
console.log(box.style.width) // box.style 这种方式只能获取或者设置行内样式

// 获取内部或者外部的样式
// 非IE浏览器写法
var width = getComputedStyle(box, false).width
console.log(width)


// 根据兼容性封装一个获取是最终生效的样式,无论是行内还是内部或者外部
// obj: 要获取样式的对象
// attr: 属性名
function getStyle (obj, attr) {
    if (obj.currentStyle) {
        // 判断Obj有currentStyle这个属性,说明使用的是IE浏览器
        return obj.currentStyle[attr]
    } else {
        // obj没有currentStyle这个属性,说明用的不是IE
        return window.getComputedStyle(obj, false)[attr]
    }
}

var height = getStyle(box, 'height')
console.log(height)

知识借鉴:https://blog.csdn.net/qq_43157612/article/details/113527822

操作DOM

'创建节点'
var oDiv = document.createElement("div");

'克隆节点'
clonedNode = Node.cloneNode(boolean) // 只有一个参数,传入一个布尔值,true表示深客隆,复制该节点和该节点下的所有子节点;false表示浅克隆,只复制该节点

'插入节点'
parentNode.appendChild(childNode);  // 将新节点追加到子节点列表的末尾
parentNode.insertBefore(newNode, targetNode);  //将newNode插入targetNode之前
 
'替换节点'
parentNode.replaceChild(newNode, targetNode);    //使用newNode替换targetNode

'移除节点'
parentNode.removeChild(childNode);  // 移除目标节点
node.parentNode.removeChild(node);    //在不清楚父节点的情况下使用

childNode.remove()  // IE不支持

创建、克隆元素

 .box {
      width: 200px;
      height: 200px;
      background: red;
    }
  </style>
</head>
<body>

  <script>
    // 创建节点
    var box = document.createElement('div')
    box.innerHTML = 'box4'
    box.classList.add('box')
  
    // appendChild方法来往body的结束去追加box作为body的最后一个子元素。注意:反复append同一个节点只会添加一个
    // 克隆分为深克隆和浅克隆, 浅克隆只克隆元素本身, 不会克隆子元素和内容, 默认是浅克隆
    // cloneNode方法有一个参数,默认值是false,代表浅克隆,true代表深克隆,就会连着内容一起克隆
    var box1 = box.cloneNode(true)
    document.body.appendChild(box1)
  </script>

插入元素

  <ul>
    <li>首页</li>
    <li>列表</li>

    <li id="about">关于</li>
  </ul>
  <script>
    var ul = document.querySelector('ul')
    var li = document.createElement('li')
    var about = document.querySelector('#about')
    li.innerHTML = '联系'
    ul.insertBefore(li, about) // 把li放在ul里面, about前面
  </script>

替换元素

 <ul>
    <li>首页</li>
    <li>列表</li>
    <li id="about">关于</li>
  </ul>
  <script>
    var ul = document.querySelector('ul')
    var about = document.querySelector('#about')
    var li = document.createElement('li')
    li.innerHTML = '联系我们'

    // 在ul里面把about替换成li (节点替换)
    ul.replaceChild(li, about)
  </script>

删除元素

  <ul>
    <li>首页</li>
    <li>列表</li>
    <li id="about">关于</li>
  </ul>
  <script>
    var ul = document.querySelector('ul')
    var about = document.querySelector('#about')

    // 父元素.removeChild删除子元素
    // ul.removeChild(about)

    // 补充:parentNode找到自己的父级
    // about.parentNode.removeChild(about)

    // 可以自己删除,但是IE8-不能使用
    about.remove()

  </script>

创建文档碎片

var cache = document.createDocumentFragment();
for( var i = 0 ; i < 1000; i ++ ){
   var opt = document.createElement("input");
   opt.type="button";
   opt.value = "删除";
   cache.appendChild(opt);
}
document.body.appendChild(cache);
"常规方法"
    // 这种写法创建10个li,append 10次,进行了10次DOM操作,这样反复执行性能不好,量大可能造成卡顿
    var ul = document.querySelector('ul')

    for (var i = 0; i < 10; i++) {
        var li = document.createElement('li')
        li.innerHTML = i+1
        ul.appendChild(li)
    }


'使用创建文档碎片方法'
    // 一个文档碎片先把10个li放入碎片里,然后只需要一次把碎片插入ul就行了,这样就只有一次DOM操作了,这种写法虽然看起来更麻烦,但是减少了DOM操作的次数,性能更优
    var cache = document.createDocumentFragment()
    for (var i = 0; i < 10; i++) {
        var li = document.createElement('li')
        li.innerHTML = i+1
        // li放到文档碎片(cache)里
        cache.appendChild(li)
    }
    ul.appendChild(cache)

获取元素大小

box.clientWidth
box.clientHeight
	返回了元素大小,但没有单位,默认单位是px,如果设置了其他的单位,比如100em之类,返回出来的结果还会转换为px像素(不含边框)

box.scrollWidth
box.scrollHeight
	获取滚动内容的元素大小(当元素出现滚动条时,此属性指全部滚动内容的宽高)返回了元素大小,但没有单位,默认单位是px。如果没有设置任何CSS的宽和高度,它会得到计算后的宽度和高度,使用最频繁,获取的是可滚动内容的总高度,如果box没有滚动条,跟clientHeight是一样的,不包含边框 

box.offsetWidth
box.offsetHeight
	返回了元素大小,但没有单位,默认单位是px。如果没有设置任何CSS的宽和高度,他会得到计算后的宽度和高度包含盒模型中除margin以外的宽高(包含边框)最稳定
"示例"

.box {
    width: 200px;
    padding: 20px;
    border: 5px solid #333;
    margin: 10px;
    height: 200px;
    background: red;
    overflow: auto;
    }

<div class="box"></div>
var box = document.querySelector('.box')



'offsetWidth'
    // 包含padding、border,不包含margin
    var width1 = box.offsetWidth
    console.log(width1)

'clientWidth'
    // clientWidth就是在offsetWidth的基础上减去border
    var width2 = box.clientWidth
    console.log(width2)

'scrollHeight'
    // 一般都是纵向滚动条,所以以高度为例
    // 获取的是可滚动内容的总高度,如果box没有滚动条,跟clientHeight是一样的
    // 内容超出的时候获取的总内容高度
    // 不包含边框的
    var height = box.scrollHeight
    console.log(height)

注意:这三对属性都只是用来获取,不能设置,如果要修改元素尺寸,还是用样式来修改,带单位的字符串,
	如:box.style.width = '300px'

获取元素位置

box.clientLeft 获取左边框宽度
box.clientTop  获取上边框的宽度

box.offsetLeft 获取元素当前相对于offsetParent父元素的左位置
box.offsetTop  获取元素当前相对于offsetParent父元素的上位置

box.scrollTop  获取滚动内容上方的位置(就是隐藏的内容)
box.scrollLeft 获取滚动内容左方的位置(就是隐藏的内容)

offsetParent 这个属性的返回值是它根据谁定位的,如果它的所有父元素都没有定位,那么返回body
<script>
    var box = document.querySelector('.box')
    // 获取的是元素加边框前后的偏移量,也就是左边框的宽度
    // clientTop 就是上边框的宽度
    console.log(box.clientLeft)

    // offsetParent指的是最近的有定位属性的祖先级,如果都没有定位,得到body
    // offsetLeft获取的就是相对于offsetParent的坐标
    // 如果box有绝对定位的话,那么offsetLeft就和定位坐标left的值一样
    console.log(box.offsetLeft)

    // console.log(box.scrollTop) // 在这里直接获取永远都是0
    // 当滚轮开始滚动的时候来获取
    box.onscroll = function () {
      console.log(box.scrollTop)
    }

    // 让滚轮默认在元素底部,可以给scrollTop赋值
    // 滚动内容总高度 - 盒子本身高度可以让滚动条在最底部
    box.scrollTop = box.scrollHeight - box.clientHeight // 让滚动条在最底部的公式
  </script>

事件

"w3c标准:事件写在行内,但是因为结构和行为要分离,所以我们一般情况下用JavaScript的方法来绑定事件,只有在极少数情况下,才将事件写在行内"

1.当用户跟页面进行一些"交流"的时候,页面通过js就会触发一些事件,比如鼠标点击的时候就会触发onclick事件,给这个事件绑定一个函数,那么这个时候函数就会被调用,代码就会被执行,这一操作就是一个事件
2.当事件被触发的时候对象会得到一个信息(事件对象),包含了跟事件相关的一些属性和方法的封装(如:事件发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态等),只有事件在触发的时候才会得到。
3.事件是给浏览器定义一个预处理函数,当事件触发的时候,执行函数,这就是事件。事件类型有很多种,大体上有鼠标事件、键盘事件、表单事件、非用户行为...等等

'事件的绑定方法':
    浏览器中的节点(对象).on+事件句柄 = function( ){
       要干什么?(放在**浏览器中**,**不执行**,当事件发生的时候**再执行**。)
    }
    oDiv.onclick=function(){   
      alert(1)
    }


"表单事件":
    onblur  元素失去焦点
    onfocus 元素获得焦点
    oninput 用户输入时触发
    onchange 用户改变域内容(值改变时触发)
    onsubmit form提交时触发

"键盘事件":
    onkeydown 键盘某个键被按下
    onkeyup 键盘某个键被松开
    onkeypress 键盘某个键被按下并松开

"鼠标事件":
    onclick 鼠标点击某个对象
    ondblclick 鼠标双击某个对象
    onmousedown 某个鼠标按键被按下
    onmousemove 鼠标在某元素上移动
    onmouseup 某个鼠标按键被松开
    onmouseenter 鼠标移到某元素之上(1)
    onmouseleave 鼠标从某元素移开(2)
    onmouseover 鼠标移到某元素之上(1代替)
    onmouseout 鼠标从某元素移开(2代替)

"非用户行为":
onload  某个页面或图像被完成加载
计时器
.....


例子:

1. 给input元素绑定(注册)一个失去焦点的事件
<input type="text">
var ipt = document.querySelect('input');
ipt.onblur = function (){//事件处理函数
    console.log( aIpt[0].value );
}
    
    

2. onsubmit form提交时触发   
<form action="https://www.baidu.com/" method="get">
账号:<input type="text" name="username"><br/>
密码:<input type="text" name="password"><br/>
性别:<select name="sex">
        <option value="0">女</option>
        <option value="1">男</option>
        <option value="2">未知</option>
      </select><br/>
<input type="submit" value="注册">
</form>

<script>
var form = document.querySelector('form');
form.onsubmit = function (){
    alert('注册成功');
}
</script>


3. onload  某个页面或图像被完成加载
<script>
window.onload = function (){ // 页面加载完成时触发(html, css, js , 图片,音视频等资源加载完成才执行)
    var box = document.querySelector('.box');
    console.log(box);
}
</script>
onmouseover和onmouseenter都是鼠标移入该元素的时候触发的事件,但是,不一样的地方是,如果打开页面的时候鼠标刚好在该元素上面,onmouseenter就没有被触发,而onmouseover会被触发,因为onmouseenter只有在鼠标进入该元素的瞬间才会被触发

onmouseleave和onmouseout的区别也是如此,只要鼠标不在该元素身上,onmouseout就会触发,而onmouseleavr只有在鼠标从该元素身上离开的时候才会触发

事件与函数的关系

JavaScript是事件驱动的语言!
1.事件通常与函数配合使用,当事件发生时执行对应的函数。
2.事件依赖于函数执行,函数可以由事件驱动执行。

表单事件

<form id="form" action="http://www.baidu.com">
    <input type="checkbox">
    <select name="" id="city">
        <option value="1">北京</option>
        <option value="2">武汉</option>
        <option value="3">成都</option>
        <option value="4">深圳</option>
    </select>
    <input type="text" id="text">
    <button>提交</button>
</form>

<script>
    // 在单选,多选,下拉菜单给这些元素绑事件一般用change
    var oCity = document.querySelector('#city')
    oCity.onchange = function () {
        console.log(this.value)
    }

    var oText = document.querySelector('#text')
    // focus 聚焦 获取焦点事件
    oText.onfocus = function () {
        console.log('获取焦点了')
    }

    // 失去焦点
    oText.onblur = function () {
        console.log('失去焦点了')
        console.log(this.value)
    }

    // 表单提交事件submit
    var oForm = document.querySelector('#form')
    oForm.onsubmit = function () {
        // 在表单提交之前做一些处理
        // 比如文本框没有内容就不提交
        if (oText.value === '') {
            // 不允许表单提交
            // return false可以阻止表单提交
            console.log('提交3')
            return false
        }
        console.log('提交')
    }
</script>

鼠标事件及事件对象

事件对象有兼容问题,所以我们先做好兼容再去使用事件对象: e=e || window.event;

e.buttons            返回鼠标点击按键(1左键,2右键,4中键滚轮) 
e.offsetX / offsetY  获取事件触发距离最近的盒子的坐标            
e.clientX / clientY  获取可视区的坐标(根据浏览器的定位)        
e.screenX / screenY  获取整个屏幕的坐标                          
e.pageX / e.pageY    获取文档的坐标(包含滚动条)                
<head>
  <style>
    #box {
      width: 200px;
      height: 200px;
      background: red;
      padding: 30px;
    }

    #son {
      width: 100px;
      height: 100px;
      background: pink;
    }
  </style>
</head>

<body style="height: 10000px">
  <div id="box">
    <div id="son">son</div>
  </div>
</body>

<script>
  var oBox = document.querySelector('#box')
  // event 事件
  // 事件处理函数的第一个参数默认就是事件对象
  oBox.onmousedown = function (e) {
    // 事件对象的兼容
    e = e || window.event
    console.log(e)
  }

  var oSon = document.querySelector('#son')

  oBox.onclick = function (e) {
    e = e || window.event
    // offsetX 表示鼠标指针位置相对于触发事件的对象的 x 坐标。
    // offsetY 表示鼠标指针位置相对于触发事件的对象的 y 坐标。
    console.log("offsetX", e.offsetX, e.offsetY)

    // 无论点box的哪个位置,都是固定算到浏览器可视区边缘,不计算滚动部分
    console.log("clientX", e.clientX, e.clientY)

    // 无论浏览器是多大,都是获取到整个屏幕边缘的坐标
    console.log("screenX", e.screenX, e.screenY)

    // 如果没有滚动条或者滚动条在最顶部,那么这组值和client是一样的
    // 有滚动条他是计算包含滚动部分的总值  (通俗讲就是鼠标所在位置在文档的那个位置)
    console.log('pageX:', e.pageX, e.pageY)

    // client和page这两对用的最多

  }
</script>

键盘事件

键盘事件:keydown、keyup、keypress

document.onkeydown = function(e){
    e = e || window.event
    console.log(e.keyCode)  
}

键盘上每一个键都有一个唯一的编码,用来识别当前用户正在操作的是键盘上哪一个键

有兼容问题(解决)
e.keyCode || e.which

特殊键码:是否按下alt  ctrl  和 shift
e.altKey
e.ctrlKey
e.shiftKey
返回值是布尔值;
可以用来判断组合键
if(e.keyCode==13&&e.altKey){
    alert('同时按下了enter和alt');
}
  <script>
    document.onkeydown = function (e) {
      e = e || window.event
      // 获取键盘键码的兼容写法
      var code = e.keyCode || e.which
      console.log(code)
      console.log(e.ctrlKey)

      if (e.ctrlKey && code === 83) {
        // s的键码是83
        console.log('同时按下了ctrl和s')
      }
    }
  </script>

事件冒泡

Netscape认为,石头先扔进河里,再从河里确定了一个扔石头的点,从外往内逐渐精确的过程(捕获)
w3c认为,石头扔进去先到达准确的那个点,涟漪从内往外扩散(冒泡)

事件流:事件执行的顺序
	子元素的事件被触发时(子元素没有事件时也可以触发事件冒泡),父级事件也会被触发(冒泡)
	一个完整事件包含 捕获阶段 ---> 目标阶段 --->冒泡阶段

冒泡是可以阻止的
   e.stopPropagation( );
   e.cancelBubble = true; // 兼容IE

'例子'

<head>
  <style>
    #yeye {
      width: 400px;
      height: 400px;
      background-color: pink;
    }

    #father {
      width: 200px;
      height: 200px;
      background-color: red;
    }

    #son {
      width: 100px;
      height: 100px;
      background-color: blue;
    }
  </style>
</head>

<body>
  <div id="yeye">
    <div id="father">
      <div id="son"></div>
    </div>
  </div>
</body>
<script>
  // 事件冒泡是从自己开始一层一层往外传播事件的过程
  // 一个完整事件流一定是包含捕获 -> 目标 -> 冒泡 这样一个完整过程,但是浏览器在处理事件的时候默认在冒泡阶段处理
  // IE浏览器不认识捕获,只能在冒泡阶段处理
  document.onclick = function () {
    console.log('document')
  }
  document.querySelector('#yeye').onclick = function () {
    console.log('yeye')
  }
  document.querySelector('#father').onclick = function (e) {
    console.log('father')
    e = e || window.event

    // 如果某些情况下不希望冒泡,那么可以阻止
    // 阻止冒泡的兼容写法:阻止事件从当前元素往外传播
    if (e.stopPropagation) {
      e.stopPropagation()
    } else {
      e.cancelBubble = true
    }
  }
  document.querySelector('#son').onclick = function () {
    console.log('son')
  }

</script>

事件绑定事件监听

DOM0级事件处理,是一种赋值方式,是被所有浏览器所支持的,简单易懂容易操作;
DOM2级事件处理是所有DOM节点中的方法,可以重复监听,但是浏览器兼容存在问题;
//DOM0级
oDiv.onclick = function(){ .... }

//DOM2级
if(window.attachEvent){
   oDiv.attachEvent("onclick", function(){ ... }); // IE只有冒泡阶段,所以没有第三个参数,而且需要加on;
}else{
   oDiv.addEventListener( "click", function(){ ... },false); // false指冒泡阶段
}
//移除事件监听,第二个参数为必须,移除的事件处理函数
oDiv.removeEventListener( "click",fn)
oDiv.detachEvent("onclick",fn)
  监听也存在事件冒泡
  <div id="box">box</div>
  <script>
    var oBox = document.querySelector('#box')

    // 绑定事件(DOM0级)的触发阶段默认都是冒泡
    oBox.onclick = function () {
       console.log('box')
    }

    // 事件监听(DOM2级)
    // 1个参数是不带on的事件类型的字符串
    // 2个参数传事件处理函数
    // 3个参数是否捕获,true代表在捕获阶段处理事件,false冒泡,默认值为false
    oBox.addEventListener('click', function () {
      console.log('box')
    }, true)

    document.addEventListener('click', function () {
      console.log('document')
    }, true)
  </script>

事件重复监听

 <div id="box">box</div>
  <script>
    var oBox = document.querySelector('#box')
    // 绑定DOM0级对于添加事件的方式
    // 一个元素的同类型事件只能绑定一个,如果多次绑定后面的覆盖前面的
    oBox.onclick = function () {
      console.log('box2')
    }
    oBox.onclick = function () {
      console.log('box0')
    }

    // 监听DOM2级对于事件的处理
    // 可以给一个元素监听多个同类型事件,他们都会触发
    // 先监听,先执行
    // 开发中一般都用监听
    oBox.addEventListener('click', function () {
      console.log('box1')
    })
    oBox.addEventListener('click', function () {
      console.log('box2')
    })

  </script>

取消事件与取消监听

 <button>取消事件</button>
  <div id="box">box</div>
  <script>
    var btn = document.querySelector('button')
    var oBox = document.querySelector('#box')
    
	'DOM0级'
    // 绑定与解除绑定
    oBox.onclick = function () {
      console.log('box')
    }
    // 点击按钮的时候把box的事件解除绑定
    btn.onclick = function () {
      // 重新赋值为null,就可以把原来的函数覆盖,就没有事件了
      oBox.onclick = null
    }


	'DOM2级'
    // 监听与取消监听
    function fn () {
      console.log('box1')
    }
    oBox.addEventListener('click', fn)
    oBox.addEventListener('click', function () {
      console.log('box2')
    })
    btn.addEventListener('click', function () {
      // 移出监听的函数和添加监听必须是同一个函数(地址相同)
      oBox.removeEventListener('click', fn)
    })
  </script>

事件添加和移出封装

/**
 * 添加事件监听
 * @param ele         <DOMObject> 添加事件的DOM元素
 * @param type        <string>    事件类型(不带on)
 * @param fn          <function>  事件处理函数
 * @param [isCapture] <boolean>   可选参数,是否捕获,true代表捕获,false代表冒泡,默认为false
 */
function on (ele, type, fn, isCapture) {
    // 如果参数没有传,默认值为false
    if (isCapture === undefined) isCapture = false
    if (ele.attachEvent) {
        // IE
        ele.attachEvent('on' + type, fn)
    } else {
        ele.addEventListener(type, fn, isCapture)
    }
}

function on({ ele, type, fn, iscapture = false }) {
    if (ele.attachEvent) {
        ele.attachEvent('on' + type, fn)
    } else {
        ele.addEventListener(type, fn, iscapture)
    }
}


/**
 * 移出事件监听
 * @param ele         <DOMObject> 添加事件的DOM元素
 * @param type        <string>    事件类型(不带on)
 * @param fn          <function>  事件处理函数
 * @param [isCapture] <boolean>   可选参数,是否捕获,true代表捕获,false代表冒泡,默认为false
 */
function off (ele, type, fn, isCapture) {
    // 如果参数没有传,默认值为false
    if (isCapture === undefined) isCapture = false
    if (ele.detachEvent) {
        ele.detachEvent('on' + type, fn)
    } else {
        ele.removeEventListener(type, fn, isCapture)
    }
}



<button>移出监听</button>
<div id="box">box</div>
<script>
    var btn = document.querySelector('button')
var oBox = document.querySelector('#box')
function foo () {
    console.log('box1')
}
on(oBox, 'click', foo)

on(btn, 'click', function () {
    off(oBox, 'click', foo)
})

</script>

事件委托

什么是事件委托:全班同学都要做《五年高考三年模拟》,每个同学自己去书店买?不用,委托老师帮你们买,同学们只管认真做题就好啦,好开心。
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以处理某一类型的所有事件
使用场景主要用于事件源不确定的情况,可以把事件委托给父级

不用给子元素一个一个设置事件,而只给父元素设置,从而十分的方便
判断事件源:
    e.target || e.srcElement

事件委托的原理:事件冒泡
事件委托优点:
    1、方便
    2、可以给不确定的元素添加事件(后创建的)
<head>
  <style>
    li {
      width: 200px;
      height: 30px;
      margin: 10px 0;
      background-color: red;
    }
  </style>
</head>

<body>
  <ul>
    <li>许嵩</li>
    <li>周杰伦</li>
    <li>陈奕迅</li>
    <!-- <li><a href="#">林俊杰</a></li> -->
    <li>林俊杰</li>
  </ul>
  <input type="text" id="name">
  <button id="add">添加</button>
</body>
<script>
  var inputName = document.querySelector('#name')
  var addBtn = document.querySelector('#add')
  on(addBtn, 'click', function () {
    var li = document.createElement('li')
    li.innerHTML = inputName.value
    oUl.appendChild(li)
  })
</script>

默认行为

有一些html元素默认的行为
    比如: a标签,点击后有跳转动作
         form表单中的submit类型的input有一个默认提交跳转事件
         reset类型的input有重置表单行为。
    有些时候我们是不需要默认事件的,所以就需要阻止默认事件    



"不同的事件里阻止的是不同的行为"
if (e.preventDefault) {
    e.preventDefault()
} else {
    return false
}

正则

知识借鉴:
    1.https://blog.csdn.net/fd2025/article/details/125326832
    2.https://www.runoob.com/w3cnote/regular-expression-30-minutes-tutorial.html
    3.https://es6.ruanyifeng.com/#docs/regex
    4.https://www.runoob.com/jsref/jsref-obj-regexp.html
1.正则表达式是"Regular expression"(规则表达式),就是"定义字符串规则的表达式",语法基于一种古老的perl语言,它描述了一种字符串的匹配模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出复合某个条件的子串等,正则本身是独立于语言之外的,很多语言都含有。
2.正则表通常被用来检索、替换那些符合某个模式(规则)的文本,例如验证表单:用户名表单只能输入英文字母、数字或者下划线, 昵称输入框中可以输入中文(匹配)。此外,正则表达式还常用于过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等
3.在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码

'特点'
1.灵活性、逻辑性和功能性非常的强。
2.可以迅速地用极简单的方式达到字符串的复杂控制。
3.对于刚接触的人来说,比较晦涩难懂。比如:^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*$
4.实际开发,一般都是直接复制写好的正则表达式. 但是要求会使用正则表达式并且根据实际情况修改正则表达式. 比如用户名: /1{3,16}$/

'创建'
	'方式一' 常用
	var regex = new RegExp('xyz', 'i');
    // 等价于
    var regex = /xyz/i; // 这种类型里面不需要加引号 不管是数字型还是字符串型

	'方式二'
	var regex = new RegExp(/xyz/i);
    // 等价于
    var regex = /xyz/i;

	'方式三'
	// 返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符
	var regex = new RegExp(/xyz/ig, 'i')
    // 等价于
    var regex = /xyz/i;

方法

' exce'
1.exec() 方法用于检索字符串中的正则表达式的匹配,如果字符串中有匹配的值返回该匹配值构成的数组,检索字符串中指定的值。返回找到的值,并确定其位置。
2.该数组还有继承的属性:
    index:表示第一个匹配的字符在原字符串中的位置,
    input:表示原字符串,
    groups:表示当初中命名的分组时匹配到的分组对象;
3.exec()方法没有匹配到数据时返回 null。
	' 当正则匹配中没有分组时'
        const str="努力666 123 学习! 456 的打工人! qrw";
        const patt=/\d+/;
        const result=patt.exec(str);
        console.log(result)

	'当正则匹配中有分组且分组存在名字时'
        const str="努力666 123 学习! 456 的打工人! qrw";
        const patt=/(?<努力>\d)+/;
        const result=patt.exec(str);
        console.log(result)

    '没有匹配到符合正则的字符时'
        const str="努力学习的汪";
        const patt=/\d+/;
        const result=patt.exec(str);
        console.log(result)  //返回 null

'test'
    test()正则对象方法,用于检测字符串是否符合该规则,该对象会返回 true 或 false,其参数是测试字符串。

    var rg = /123/;
    console.log(rg.test(123));//匹配字符中是否出现123  出现结果为true
    console.log(rg.test('abc'));//匹配字符中是否出现123 未出现结果为false

'compile'  // 目前在 1.5版本已废弃
    该方法的作用是能够对正则表达式进行编译,被编译过的正则在使用的时候效率会更高,"适合对一个正则多次调用的情况使用",如果一个正则只使用一两次,那么该方法没有特别显著的效果
    var reg = /[abc]/gi
    console.log(reg.test('a'))

    reg = /[cde]/gi
    reg.compile(reg)
    console.log(reg.test('a'))
    console.log(reg.test('a'))

特殊字符

[]      匹配其中某一个字符
[abcde] 匹配abcde其中任意一个
[a-z]   匹配所有小写字母
[0-9]   匹配任意一个数字
()      分组、小括号里面的内容作为整体进行匹配
\	    转码字符
|       或,跟js中的(||)一样

'[]'
    字符集也叫做字符类。 方括号用来指定一个字符集。 在方括号中使用连字符来指定字符集的范围。 在方括号中的字符集不关心顺序,如[a-z0-9A-Z_]或[a-z0-9_A-Z]等等都表示\w含义
        '匹配.'
        var rex = /hello[.]/;
        var result = rex.test('hello.world')
        console.log(result) // true
		单个字符匹配: 直接书写
		字符范围匹配: 使用连字符 (-)
		字符并集匹配: 使用竖线 (|)
		注意事项:
            1. 方括号中的字符顺序不影响匹配结果。例如,[abc]和[bac]在匹配时是等效的。
            2. 方括号中的特殊字符会失去其特殊含义。例如,方括号中的点号(.)表示匹配点号本身,而不是通配符。
            3. 方括号中的连字符(-)在放置在开头或结尾时可以表示普通字符。例如,[-abc]表示匹配字符-、a、b或者c。
            4. 方括号中的字符集合不会被重复匹配。例如,[abcabc]和[abc]在匹配时是等效的。

'边界符(位置符)'
    ^  在[]中表示排除(除了)类似js中的(!),不在[]中表示匹配字符串开头;^也匹配 '\n' 或 '\r' 之后的位置。
    $  匹配字符串结尾,/^$/如果 ^和 $ 在一起表示完整匹配(精确匹配);$也匹配 '\n' 或 '\r' 之前的位置。
	\b	匹配单词边界

'小括号'
	(...) 中包含的内容将会被看成一个整体

'\'
    反斜线 \ 在表达式中用于转码紧跟其后的字符。用于指定 { } [ ] / \ + * . $ ^ | ? 这些特殊字符。如果想要匹配这些特殊字符则要在其前面加上反斜线 \

"注意:小括号、竖线不要放在[]内,因为没有意义"

元字符

\d  数字 [0-9]
\D	非数字 [^0-9]
\w	数字、字母、下划线 [a-z0-9A-Z_]
\W	非数字、字母、下划线 [^a-z0-9A-Z_]
\s	匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S	匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
.	匹配除换行符(\n、\r)之外的任何单个字符
\b	匹配单词边界
\B	匹配非单词边界
\0	匹配Nul字符  ??
\cx 匹配由 x 指明的控制字符。例如\cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\n	匹配换行符  等价于 \x0a 和 \cL 
\f	匹配换页符  等价于 \x0c 和 \cJ
\r	匹配回车符  等价于 \x0d 和 \cM
\t	匹配制表符  等价于 \x09 和 \cI
\v  匹配一个垂直制表符。等价于 \x0b 和 \cK
\p	匹配 CR/LF(等同于 \r\n),用来匹配 DOS 行终止符


'边界量词符'
^、$、\b、\B
	'\b'
        /\bis\b/
        My name is hongjilin
        my name@is@hong jilin
        myname学is习hongjilin
        mynameishongjilin //只有此处不被匹配
      '\B'
        /\Bis\B/
        My name is hongjilin
        my name@is@hong jilin
        myname学is习hongjilin
        mynameishongjilin //只有此处被匹配,与单词边界切好相反   

量词符

'重复次数'
{n}    匹配n次
{n,m}  最少n次,最多m次
{n,}   最少n次,最多不限
+	   最少1次,最多不限 		 {1,}
?	   可有可无,最多一个		{0,1}
*	   可以有也可以没有,个数不限  {0,}

修饰符

i	匹配不区分大小写
s	使.可以匹配任意单个字符;更改.的含义,使它与每一个字符匹配(包括换行符\n、回车符\r、行分隔符、段分隔符)
g	执行全局匹配(查找所有匹配而非在找到第一个匹配后停止) 
m	执行多行匹配,必须与g一起使用,并且当使用^或$模式时才会起作用;更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
u	处理字符码点大于0xFFFF的 Unicode 字符
d	可以让exec()、match()的返回结果添加indices属性,在该属性上面可以拿到匹配的开始位置和结束位置


'g'
常用于执行一个全局搜索匹配,即(不仅仅返回第一个匹配的,而是返回全部)
    "/学习的汪/" => 只匹配到第一行的 学习的汪
    `努力学习的汪
    努力学习的汪
    非常努力学习的汪`

    "/学习的汪/g" => 三行都匹配到学习的汪
    `努力学习的汪
    努力学习的汪
    非常努力学习的汪`

'i' 
用于忽略大小写
    "/Hong/gi" =>  三行都匹配到
    //默认情况下是大小写敏感的,但此处这样标志后,就成为忽略大小写
    `hongjilin
    Hongjilin
    HONGJILIN
    hOngjilin`
    
'm'
常用于执行一个多行匹配。像之前介绍的 (^,$) 用于检查格式是否是在待检测整个字符串的开头或结尾。但我们如果想要它在每行的开头和结尾生效,我们需要用到多行修饰符 m。
    "/学习的(汪|打工人)$/g" => // 匹配不到
    `努力学习的汪
    努力学习的打工人
    努力学习的打工人 hongjilins
    努力学习的汪_`  //此处 _ 模拟表示空格

    "/学习的(汪|打工人)$/gm" => // 每行都匹配到
    `努力学习的汪
    努力学习的打工人
    努力学习的打工人 hongjilins
    努力学习的汪_`  //此处 _ 模拟表示空格 
    
'u'
含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
    /^\uD83D/u.test('\uD83D\uDC2A') // false
    /^\uD83D/.test('\uD83D\uDC2A') // true
	// \uD83D\uDC2A是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false。

	'.(点字符)'
     对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符
        var s = '';
        /^.$/.test(s) // false
        /^.$/u.test(s) // true
        // 如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败

	'Unicode 字符表示法'
	ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词
    	/\u{61}/.test('a') // false
        /\u{61}/u.test('a') // true
        /\u{20BB7}/u.test('') // true
		// 如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u
	'量词'
    使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
        /a{2}/.test('aa') // true
        /a{2}/u.test('aa') // true
        /{2}/.test('') // false
        /{2}/u.test('') // true

'y'
叫做“粘连”(sticky)修饰符,y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
	var s = 'aaa_aa_a';
    var r1 = /a+/g;
    var r2 = /a+/y;

    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]

    r1.exec(s) // ["aa"]
    r2.exec(s) // null
	'lastIndex'属性,lastIndex属性指定每次搜索的开始位置
        const REGEX = /a/g;
        // 指定从2号位置(y)开始匹配
        REGEX.lastIndex = 2;
        // 匹配成功
        const match = REGEX.exec('xaya');
        // 在3号位置匹配成功
        match.index // 3
        // 下一次匹配从4号位开始
        REGEX.lastIndex // 4
        // 4号位开始匹配失败
        REGEX.exec('xaya') // null

's'
使.可以匹配任意单个字符
    正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

    所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”。

    U+000A 换行符(\n)
    U+000D 回车符(\r)
    U+2028 行分隔符(line separator)
    U+2029 段分隔符(paragraph separator)
    /foo.bar/.test('foo\nbar')
    // false
    上面代码中,因为.不匹配\n,所以正则表达式返回false。但是,很多时候我们希望匹配的是任意单个字符,这时有一种变通的写法。

    /foo[^]bar/.test('foo\nbar')
    // true
    这种解决方案毕竟不太符合直觉,ES2018 引入s修饰符,使得.可以匹配任意单个字符。

    /foo.bar/s.test('foo\nbar') // true
    这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

    const re = /foo.bar/s;
    // 另一种写法
    // const re = new RegExp('foo.bar', 's');

    re.test('foo\nbar') // true
    re.dotAll // true
    re.flags // 's'
    /s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^和$匹配每一行的行首和行尾。

'd'
可以让exec()、match()的返回结果添加indices属性,在该属性上面可以拿到'整个匹配'结果的开始位置、结束位置和'组匹配的每个组'的开始位置、结束位置
    const text = 'zabbcdef';
    const re = /ab/d;
    const result = re.exec(text);
    result.index // 1
    result.indices // [ [1, 3] ]

    const text = 'zabbcdef';
    const re = /ab+(cd)/d;
    const result = re.exec(text);
    result.indices // [ [ 1, 6 ], [ 4, 6 ] ]

    const text = 'zabbcdef';
    const re = /ab+(cd(ef))/d;
    const result = re.exec(text);
    result.indices // [ [1, 8], [4, 8], [6, 8] ]


    如果正则表达式包含具名组匹配,indices属性数组还会有一个groups属性。该属性是一个对象,可以从该对象获取具名组匹配的开始位置和结束位置。
    const text = 'zabbcdef';
    const re = /ab+(?<Z>cd)/d;
    const result = re.exec(text);
    result.indices.groups // { Z: [ 4, 6 ] }
    上面例子中,exec()方法返回结果的indices.groups属性是一个对象,提供具名组匹配Z的开始位置和结束位置。

    如果获取组匹配不成功,indices属性数组的对应成员则为undefined,indices.groups属性对象的对应成员也是undefined。
    const text = 'zabbcdef';
    const re = /ab+(?<Z>ce)?/d;
    const result = re.exec(text)
    result.indices[1] // undefined
    result.indices.groups['Z'] // undefined

直接量字符

正则中有一些字符本身是具有含义的,那如果我们要匹配这个字符就需要用到\转义
/ 	匹配 /
? 	匹配 ?
\\	匹配 \
\* 	匹配 *
\+	匹配 +
\[	匹配 [
\]	匹配 ]
\}	匹配 }
\{	匹配 {
\(  匹配 (
\)  匹配 )
\.  匹配 .
\'  匹配 '
\"  匹配 "
\xxx 查找以八进制数xxx规定的字符
\xdd 查找以十六进制数dd规定的字符
\uxxxx 查找以十六进制数xxxx规定的Unicode字符

支持正则的字符串API

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

    String.prototype.match 调用 RegExp.prototype[Symbol.match]
    String.prototype.matchAll 调用 RegExp.prototype[Symbol.matchAll] (自己)
    String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
    String.prototype.search 调用 RegExp.prototype[Symbol.search]
    String.prototype.split 调用 RegExp.prototype[Symbol.split]

"均为对象上的方法"
"Symbol.match"
对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)

class MyMatcher {
  [Symbol.match](string) {
    return 'hello world'.indexOf(string);
  }
}

'e'.match(new MyMatcher()) // 1

"Symbol.replace"
对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
下面是一个例子。

const x = {};
x[Symbol.replace] = (...s) => console.log(s);

'Hello'.replace(x, 'World') // ["Hello", "World"]
Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World。

"Symbol.search"
对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)

class MySearch {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](string) {
    return string.indexOf(this.value);
  }
}
'foobar'.search(new MySearch('foo')) // 0

"Symbol.split"
对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
下面是一个例子。

class MySplitter {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](string) {
    let index = string.indexOf(this.value);
    if (index === -1) {
      return string;
    }
    return [
      string.substr(0, index),
      string.substr(index + this.value.length)
    ];
  }
}

'foobar'.split(new MySplitter('foo'))
// ['', 'bar']

'foobar'.split(new MySplitter('bar'))
// ['foo', '']

'foobar'.split(new MySplitter('baz'))
// 'foobar'
上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为

// 从Symbol目录迁移过来
'注意:JS中的字符串默认编码是UTF-16'

'search'
用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。如果找到任何匹配的子串,则返回 该子串在原字符串中的第一次出现的位置。如果没有找到任何匹配的子串,则返回 -1。
    const str1 = 'hello 123 的汪 456';
    const reg1 = /\d+/; 
    console.log(str1.search(reg1)); // 6

    const str2 = 'hello world';
    const reg2 = /\d+/;
    console.log(str2.search(reg2)); // -1

'replace'
 用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。原字符串不变,创建一个新的字符串。
	'替换一个'
    let str = 'hello word! hello word!~ hello word!~'; 
    let n = str.replace('word',"boys");
    console.log(str) // 'hello word! hello word!~ hello word!~'
    console.log(n) //  'hello boys! hello word!~ hello word!~'

	'替换全部'
    let str = 'hello word! hello word!~ hello word!~'; 
    let n = str.replace(/word/g,"boys"); // 替换全部
    let n2 = str.replace(/word/,"boys");	// 仍是替换单行
    console.log(str) // 'hello word! hello word!~ hello word!~'
    console.log(n) //  'hello boys! hello boys!~ hello boys!~'
    console.log(n2) //  'hello boys! hello word!~ hello word!~'

'split'
用于把一个字符串按符合匹配条件的方式分割成一个字符串数组,第二个参数表示数组长度。不改变原字符串
    const str="How 1are 2you 3? 4I'm 5fine! 6thanks";
    const a=str.split(" ");
    const b=str.split(" ", 2);
    const c=str.split(/\d/);
    const d=str.split(/\d/, 3);
    console.log(a);  // ["How", "1are", "2you", "3?", "4I'm", "5fine!", "6thanks"]
    console.log(b);  // ["How", "1are"]
    console.log(c);  // ["How ", "are ", "you ", "? ", "I'm ", "fine! ", "thanks"]
    console.log(d);  // ["How ", "are ", "you "]

'match'
在字符串内检索指定的值,或找到一个或多个正则表达式的匹配,这个方法的行为在很大程度上有赖于 regexp 是否具有标志 g。不改变原字符串。
    1.如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配,与exce的完全一致
    2.如果 regexp 有标志 g,它将找到全部符合正则子字符串,并返回一个数组。
    3.如果没有找到任何匹配的文本,无论有没有g,match() 将返回 null
		'没有g的正则匹配'
            const str = 'hello 123 world 456';
            const reg=/\d+/;
            const result=str.match(reg); 
            console.log(result)  // 没有 全局搜索 的时候返回与 exce() 完全一致

		'有g的正则匹配'
            const str = 'hello 123 world 456';
            const reg=/\d+/g;
            const result=str.match(reg); 
            console.log(result)  //['123', '456'] 返回数组

		'没有匹配到子字符串'
            const str = 'hello 123 world 456';
            const reg=/hi/g; // /hi/ 也是返回null
            const result=str.match(reg); 
            console.log(result)  // 匹配不到返回null

正则属性

global		判断是否设置了 "g" 修饰符
ignoreCase	判断是否设置了 "i" 修饰符
lastIndex	用于规定下次匹配的起始位置
multiline	判断是否设置了 "m" 修饰符
source		返回正则表达式的匹配模式(正文)
flags		返回正则表达式的修饰符
unicode		表示是否设置了u修饰符
sticky		表示是否设置了y修饰符。
dotAll		返回一个布尔值,表示该正则表达式是否处在dotAll模式 (详情见修饰符s)
matchAll	可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组

'mathcAll'
如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。
var regex = /t(e)(st(\d?))/g;
var string = 'test1test2test3';
var matches = [];
var match;
while (match = regex.exec(string)) {
  matches.push(match);
}

console.log(matches)
// [
//   ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"],
//   ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"],
//   ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
// ]
上面代码中,while循环取出每一轮的正则匹配,一共三轮。

ES2020 增加了String.prototype.matchAll()方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。
const string = 'test1test2test3';
const regex = /t(e)(st(\d?))/g;
for (const match of string.matchAll(regex)) {
  console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for...of循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。

遍历器转为数组是非常简单的,使用...运算符和Array.from()方法就可以了。

// 转为数组的方法一
[...string.matchAll(regex)]

// 转为数组的方法二
Array.from(string.matchAll(regex))

常用分组语法

'分组的定义'
正则表达式通过使用括号将表达式分为不同的分组,识别的方法是通过从左至右搜寻左半括号,遇到第一个左半括号时,则该左半括号与对应的右半括号所包含的内容即为第一分组,以此类推 。例如,在表达式((A)(B(C))),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)

'分组存在意义'
向后引用:在第i个分组中被匹配的字符串,可以在在正则表达式中通过\i方式再次引用,例如\b(\w+)\b\s+\1\b可以用来匹配重复的单词,像go go, 或者kitty kitty。

'分组取值'
当通过正则表达式匹配到字符串时,可以使用matcher.[i]等方式取到第i个分组所匹配到的子字符串,如果使用了 具名分组 的话也同时可以通过matcher.groups.[name]取出对应名字的字符串,如果具名组没有匹配,那么对应的groups对象内具名属性的值是undefined,如果根本就没有书写具名的话,对应的groups的值就是undefined。
'捕获'
    (exp)			匹配exp,并捕获文本到自动命名的组里
    (?<name>exp)	 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp)
    (?:exp)			 匹配exp,不捕获匹配的文本,也不给此分组分配组号,该表达式与(exp)在效果上其实应该是没有区别的,区别只是是否算作一个分组及是否保存匹配的子文本。

'零宽断言'
    (?=exp)		匹配exp前面的位置
    (?<=exp)	匹配exp后面的位置
    (?!exp)		匹配后面跟的不是exp的位置
    (?<!exp)	匹配前面不是exp的位置

'注释'
	(?#comment)	这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

捕获

'(exp)' 
匹配exp,并捕获文本到'自动命名'的组里
    \b(\w+)\b\s+\1\b
    例子1:haha_ haha_ ha // 匹配到 haha_ haha_
    例子2:haha_ haha ha // 无法匹配		
       
'(?<name>exp)'
匹配exp,并捕获文本到'名称为name'的组里,也可以写成(?'name'exp)
    \b(?<name>\w+)\b\s+\1\b
    例子1:haha_ haha_ ha // 匹配到 haha_ haha_
    例子2:haha_ haha ha // 无法匹配
    // 可以在方法exce和match匹配的数组里看到分组
    
'(?:exp)'
匹配exp,不捕获匹配的文本,也不给此分组分配组号,该表达式与(exp)在效果上其实应该是没有区别的,区别只是是否算作一个分组及是否保存匹配的子文本,作用一般来说是为了节省资源,提高效率
	比如说验证输入是否为整数,可以这样写/([0-9]*)/,但我们只是要判断规则,没必要把exp匹配的内容保存到组里,这时就可以用非捕获组了/(?:[0-9]*)/
分组
1.正则表达式使用圆括号进行组匹配。

'匿名分组' 使用的是(exp)
    '取值'
        const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
        // 上面代码中,正则表达式里面有三组圆括号。使用exec方法,就可以将这三组匹配结果提取出来。
        const matchObj = RE_DATE.exec('1999-12-31');
        const year = matchObj[1]; // 1999
        const month = matchObj[2]; // 12
        const day = matchObj[3]; // 31

	'复用' 使用\1、\2、\num 写法
        const RE_DATE = /(\d{4})-(\d{2})-(\d{2})-\1-\2-\3/;
        const matchObj = RE_DATE.exec('1999-12-31-1999-12-31');
        // 使用\1 \2 \n.. 进行复用时,其匹配这个子表达式的文本(就是匹配的内容是完全一致的),取值见上方


'具名分组' 使用的是 (?<name>exp)
	'取值'
        const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
        const matchObj = RE_DATE.exec('1999-12-31');
        const year = matchObj.groups.year; // "1999"
        const month = matchObj.groups.month; // "12"
        const day = matchObj.groups.day; // "31"
		// matchObj是数组,但是数组也是对象,所以这里可以有groups属性
		// 同时,数字序号(matchObj[1])依然有效。
		// 如果具名组没有匹配,那么对应的groups对象内具名属性的值是undefined,如果根本就没有书写具名的话,对应的groups的值就是undefined。

		'复用' 使用\k<组名>的写法
            const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
            RE_TWICE.test('abc!abc') // true
            RE_TWICE.test('abc!ab') // false

            数字引用(\1)依然有效。
            const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
            RE_TWICE.test('abc!abc') // true
            RE_TWICE.test('abc!ab') // false

            这两种引用语法还可以同时使用。
            const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
            RE_TWICE.test('abc!abc!abc') // true
            RE_TWICE.test('abc!abc!ab') // false
解构赋值和替换
'解构赋值'
    有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
    let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
    one  // foo
    two  // bar


'替换'
	'普通参数替换'
    字符串替换时,使用$<组名>引用具名组或者使用$1、$2、$num。
    let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
    '2015-01-02'.replace(re, '$<day>/$<month>/$<year>') // '02/01/2015'

    let re = /(\d{4})-(\d{2})-(\d{2})/u
    '2015-01-02'.replace(re, '$1/$2/$3')  // '02/01/2015'
    上面代码中,replace方法的第二个参数是一个字符串,而不是正则表达式。
	
    
    '函数参数替换'
    replace方法的第二个参数也可以是函数,该函数的参数序列如下。
     let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
    '2015-01-02'.replace(re, (
          ...rest
         ) => {
          console.log(rest,1);
         let {day, month, year} = rest[6];
         return `${day}/${month}/${year}`;
        });
    具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。

零宽断言

1.先行断言和后发断言都属于非捕获簇(不捕获文本 ,也不针对组合计进行计数)。 
2.断言用于判断所匹配的格式位置,匹配结果不包含该确定格式(仅作为约束)

?=	正先行断言-存在
?!	负先行断言-排除
?<=	正后发断言-存在
?<!	负后发断言-排除
正先行断言
'(?=...)'
定义一个正先行断言要使用 ()。在括号内部使用一个问号和赋值语句: (?=...),筛选条件为被匹配的字符其后跟随着断言中定义的格式

    /你好呀(?=\shello)/
    '你好呀helloWord' // 无法匹配
    ' 你好呀 helloWord' // 匹配到 '你好呀'
	'你好呀 hiBoys' // 无法匹配
负先行断言
'(?!...)'
定义一个负先行断言要使用 ()。在括号内部使用一个问号和否定语句: (?!...),筛选条件为被匹配的字符其后不跟随着断言中定义的格式
                                   
    /你好呀(?!\shello)/
    ' 你好呀helloWord' // 匹配到 '你好呀'
    '你好呀 helloWord' // 无法匹配
    '你好呀 hiBoys' // 匹配到 '你好呀'
正后发断言
'(?<=...)'
定义一个正后发断言要使用 ()。在括号内部使用一个问号和<=: (?<=...),筛选条件为被匹配的字符前跟随着断言中定义的格式
    /(?<=Word\s)你好呀/
    'helloWord你好呀' // 无法匹配
    'helloWord 你好呀' // 匹配到 '你好呀'
    'hiBoys 你好呀' // 无法匹配
负后发断言
'(?<!...)'
定义一个正后发断言要使用 ()。在括号内部使用一个问号和<!: (?<!...),筛选条件为被匹配的字符前不跟随着断言中定义的格式
    /(?<=Word\s)你好呀/
    'helloWord你好呀' // 匹配到 '你好呀'
    'helloWord 你好呀' // 无法匹配
    'hiBoys 你好呀' // 匹配到 '你好呀'

注释

通过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)。

要包含注释的话,最好是启用"忽略模式里的空白符"选项,这样在编写表达式时能"任意的添加空格"(见下方),Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:

      (?<=    # 断言要匹配的文本的前缀
      <(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
      )       # 前缀结束
      .*      # 匹配任意文本
      (?=     # 断言要匹配的文本的后缀
      <\/\1>  # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
      )       # 后缀结束

贪婪匹配与惰性匹配

正则表达式默认采用贪婪匹配模式,在该模式下意味着会匹配尽可能长的子串。

    "/(.*汪)/" => 
    努力学习的汪 非常认真读书的汪  的汪  的汪
    // 会匹配到整个 努力学习的汪 非常认真读书的汪  的汪  的汪

我们可以使用 ? 将贪婪匹配模式转化为惰性匹配模式。

    "/(.*?汪)/" => 
    努力学习的汪 非常认真读书的汪  的汪  的汪
	// 会匹配到 努力学习的汪

"可以使用量词符将贪婪匹配变成惰性匹配"
    懒惰限定符:
        *?	重复任意次,但尽可能少重复
        +?	重复1次或更多次,但尽可能少重复
        ??	重复0次或1次,但尽可能少重复
        {n,m}?	重复n到m次,但尽可能少重复
        {n,}?	重复n次以上,但尽可能少重复

Unicode属性类

详情见:https://blog.csdn.net/qq_41554403/article/details/128921424
Unicode属性查询: https://www.compart.com/en/unicode/

ES2018 引入了 Unicode 属性类,允许使用\p{...}和\P{...}(\P是\p的否定形式)代表一类 Unicode 字符,匹配满足条件的所有字符。

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true
上面代码中,\p{Script=Greek}表示匹配一个希腊文字母,所以匹配π成功。

Unicode 属性类的标准形式,需要同时指定属性名和属性值。

\p{UnicodePropertyName=UnicodePropertyValue}
但是,对于某些属性,可以只写属性名,或者只写属性值。

\p{UnicodePropertyName}
\p{UnicodePropertyValue}
\P{…}是\p{…}的反向匹配,即匹配不满足条件的字符。

注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上u修饰符。如果不加u修饰符,正则表达式使用\p和\P会报错。

由于 Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。

const regex = /^\p{Decimal_Number}+$/u;
regex.test('') // true
上面代码中,属性类指定匹配所有十进制字符,可以看到各种字型的十进制字符都会匹配成功。

\p{Number}甚至能匹配罗马数字。

// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true


下面是其他一些例子。

// 匹配所有空格
\p{White_Space}

// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配 Emoji
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

正则练习

只能用数字开头,长度在6–18位之间     
/^\d.{5,17}$/

以字母开头,数字结尾,中间任意一个字符
/^[a-z].\d$/i

密码不能少于6位的字符
/.{6,}/

以a开头 b字符至少出现2个,至多出现6个(b连续出现)
/^a[^b]*b{2,6}[^b]+/

变量的命名正则表达式(不能用数字开头 由字母、数字、下划线 、$组成)
/^[a-z_\$][\w\$]*$/i    

以010开头的座机号(后面是8位数字)
/^010-\d{8}$/

手机号以13开头,以8结尾
/^13\d{8}8$/

密码只能用6个*
/^\*{6}$/

第一位是数字,第二位是A或a,后面至多出现6个字符
/^\da.{0,6}$/i
/^\d[Aa].{0,6}$/

第一位是数字,第二位是任意一个字符,后面只能由字母、数字、下划线组成,共8位
/^\d.\w{6}$/

写出中国人姓名正则 2–4个中文
/^[\u4e00-\u9fa5]{2,4}$/

写一个qq号的正则,至少5位 至多12位数字
/^[1-9]\d{4,11}$/

邮编检验 共6位数字 第一位不能是0
/^[1-9]\d{5}$/

检验压缩包 xxx.zip或xxx.rar或xxx.tar 三个格式
/\.(rar|zip|tar)$/

手机号 1 3|4|5|6|8|7|9
/^1[3-9]\d{9}$/

账户名只能使用数字字母下划线,不能数字开头,长度在6–18之间
/^[a-z_]\w{5,17}$/i

JSON字符串

1. json对象:描述数据的一种格式,将若干繁杂的属性封装为一个整体,可以直接通过json对象,操作各个属性,json 格式是一种独立格式, 我们可以直接书写 .json 格式文件
2. json书写的就是 js 内的对象数据或者数组数据
3. json格式数据最外围要用''进行包裹(自己)

潜规则: 按照字符串内存储的内容对字符串进行了一系列的划分
    普通字符串: 'jhsagdvhjkasgvfdhj'
    数字字符串: '798798654651323265987'
    html 格式字符串: '<div></div>'
    查询字符串: 'key=value&key2=value2'


"json 格式字符串"
字符串内写的是对象: '{ "name": "Jack", "age": 18 }'
字符串内写的是数组: '[{ "name": "Jack", "age": 18 }, { "name": "Jack", "age": 18 }]'

    要求:
        1. json 格式字符串内, key 的位置必须使用 双引号包裹
        2. json 格式字符串内, 必须是 对象或者数组(数组内是一个一个的对象) 的形式
        3. json 格式字符串内, value 的位置, 如果是 纯数字或者布尔, 不需要包裹
        4. json 格式字符串内, 不能包含函数数据类型
        5. json 格式字符串内, 不允许最后一条数据以后还有一个 逗号(,)
        6. json 格式字符串内, 引号以外的内容, 只有 冒号, 逗号, 大括号, 中括号

利用 json 格式实现深拷贝: 把 obj 直接转换成 json 格式,再把转换好的 json 格式字符串在转换会对象即可
'JSON'
1.JSON与XML都是常见的数据格式JSON(JavaScript Object Notation)轻量级数据格式XML 是一种可扩展标记语言,与HTML都是标记语言
2.在数据传输流程中,JSON是以文本即字符串的形式传递的,而JS操作的是JSON对象所以,JSON对象和JSON字符串之间的相互转换是关键:	
	var json1 = '{ "name": "cxh", "sex": "man" }'; // JSON字符串
	var json2 = { "name": "cxh", "sex": "man" }; // JSON对象

image-20240210142751460

'由JSON字符串转换为JSON对象'
方法一. var obj1 = eval("(" + json + ")"; 
方法二. var obj2 = json1.parseJSON();
方法三. var obj3 = JSON.parse(json1); // 需严格的json格式
JSON.parse可以'["a","b"]'转化为["a","b"],但"['a','b']"转化会报错,但用eval("(" + json + ")",两者都不会报错
eval("(" + "['a','b']" + ")") // ['a', 'b']
eval("(" + '["a","b"]' + ")") // ['a', 'b']

因此json转数组或对象某种方法不可用可换着用

    //数组
       var arrStr = "[3,4,5]"
       console.log(JSON.parse(arrStr)) //[3,4,5]
       console.log(eval("("+arrStr+")"));//[3,4,5]
       var arrStr1 = '["3","4","5"]';
       console.log(JSON.parse(arrStr1))//["3","4","5"]
       console.log(eval("("+arrStr1+")"));//["3","4","5"]
       var arrStr2 = "['3','4','5']"
       console.log(eval("("+arrStr2+")"));//["3","4","5"]
       console.log(JSON.parse(arrStr2))//报错

       //对象
       var objStr = '{"a":1,"b":2}';
       console.log(JSON.parse(objStr))//{a: 1, b: 2}
       console.log(eval("("+objStr+")"))//{a: 1, b: 2}
       var objStr1 = "{'a':1,'b':2}";
       console.log(eval("("+objStr1+")"))//{a: 1, b: 2}
       console.log(JSON.parse(objStr1))//报错

'将JSON对象转化为JSON字符'
方法一. var str1=json2.toJSONString();
方法二. var str2=JSON.stringify(json2);

ES5

严格模式

1.严格模式是ECMAScript5的新特性,除了正常运行模式,ECMAscript 5 添加了第二种运行模式:"严格模式"(strict mode)。顾名思义,这种模式使得Javascript在更严格的条件下运行。从而使代码隐式地脱离“马虎模式/稀松模式/懒散模式”(sloppy)模式

    - 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
    - 消除代码运行的一些不安全之处,保证代码运行的安全;
    - 提高编译器效率,增加运行速度;
    - 为未来新版本的Javascript做好铺垫。

2."严格模式"体现了Javascript更合理、更安全、更严谨的发展方向,包括IE 10在内的主流浏览器,都已经支持它。

3.另一方面,同样的代码,在"严格模式"中,可能会有不一样的运行结果;一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解Javascript,让你变成一个更好的程序员。

进入严格模式 "use strict";
script标签中进入严格模式;

<script>
    "use strict"
	console.log("已经进入严格模式");
</script> 

严格模式行为变更

1.声明变量必须加var
2.全局函数的this不会指向window,而是undefined
3.函数参数名不允许重复
4.修改形参的值不会影响arguments,arguments对象不允许被动态改变; 


1、声明变量必须加var 
<script>
    "use strict"              
	a = 10;//报错 因为 a没有被var 声明
	//Uncaught ReferenceError: a is not defined; 引用错误: a 没有被声明                     
</script>


2、全局函数的this不会指向window,而是undefined
<script>
    "use strict"
    function a(){
        console.log(this)//报错 , 因为this无法指向window对象;
    }
    a();(这里运行后其this指向undefined)
    window.a()  (这里运行后this指向window)
</script>


3、函数参数名不允许重复
<script>
    "use strict";
    function a(b,b,c){ //报错
        console.log(b,b,c); // 正常模式下 2,2,3
        // 语法错误:在此上下文中不允许重复的参数名称
    }
    a(1,2,3)
</script>


4、修改形参的值不会影响arguments,arguments对象不允许被动态改变;
<script>
    function fn1(a) {
        a = 2;
        return [a, arguments[0]];
	}
    console.log(fn1(1)); // 正常模式为[2,2]

    function fn2(a) {
        "use strict";
        a = 2;
        return [a, arguments[0]];
    }
    console.log(fn2(1)); // 严格模式为[2,1]
// arguments本身表示的是实参副本,但在普通模式下会被形参改变,而加了严格模式后就不允许改变了(这里的值在简单类型上说是其值,在复杂类型上来说是其引用地址)
</script


5、严格模式新增了一些保留字,但是很多保留字再ES6里面已经实现了
implements, interface, let, package, private, protected, public, static, yield。

ES5新增常见方法

JSON.parse(str); // json序列化,将符合json格式的字符串转换为数组或对象
JSON.stringify(); // 将数组或对象转换成满足json格式的字符串

<script>
    // 一般再前后端交互的时候用的比较多
    var list = [
      {id: 1, title: '笔', price: 20, num: 2},
      {id: 2, title: '本', price: 20, num: 2},
      {id: 3, title: '书', price: 30, num: 4},
      {id: 4, title: '尺子', price: 10, num: 2}
    ]

    var str = JSON.stringify(list)
    console.log(str) // 满足json格式的字符串

    var list2 = JSON.parse(str)
    console.log(list2)

  </script>
Object.keys(obj);  // 获取obj的所有属性名称,返回数组
Object.values(obj);  // 获取obj的所有属性值,返回数组

<script>
    var obj = {
      id: 1,
      title: '笔',
      price: 20,
      num: 2
    }
    // 得到的是obj所有属性名构成的数组
    var arr1 = Object.keys(obj)
    console.log(arr1)

    // 得到的是obj所有属性值构成的数组
    var arr2 = Object.values(obj)
    console.log(arr2)

  </script>

ES6

1. ECMAScript就是JavaScript
2. js每年的6月份发布一个新版本

知识借鉴:https://es6.ruanyifeng.com/#docs/intro

let/const

let/const块级作用域:一种普遍存在于各个语言中的作用域范围
  我们可以发现,在一个大括号中用let声明的变量在外部不可访问了,每个大括号都是独立的作用域
    if (true) {
     var a = 10
      // b是一个块级变量,只有在当前块里才能使用
      let b = 20
    }
    console.log(a)
	console.log(b)

let不存在声明提升,暂时性死区

  1、let没有变量提升(JS中var声明一个变量是存在声明提升的,这是JS中的一个缺陷所在, 但是现在的let不存在声明提升;)
    console.log(a)
    let a = 10
    var a = 10
    
  2、暂时性死区
    let a = 10
    if (true) {
	 // let 声明的变量在声明前使用会直接报错
      console.log(a)
      let a = 20
const声明常量  声明就必须马上赋值
      // const声明常量,不能被重新赋值
      const a = 20
      a++
      console.log(a)
      // const定义引用类型的数据的时候,可以修改数据内部结构,但是不能修改地址
      // 因为对象是引用类型的,P中保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。即使对象的内容没发生改变,指针改变也是不允许的。但原则上const定义的就不要修改了
 

"ES6规定在某个区块中, 一旦用let或const声明一个变量,那么这个区块就变成块级作用域,用let 或者const声明的变量即为该区块绑定, 该变量不受任何变量影响。 在该变量使用let声明前不可以用。在语法上,我们叫这种情况为:暂时性死区 (temporal dead zone,简称 TDZ)"

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

"总结"
	1.var命令和function命令声明的全局变量,是顶层对象的属性(浏览器环境是window对象,node环境是global对象)
	2.let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性

globalThis对象(顶层对象)

"ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this"

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在不同运行环境里面是不统一的。
    1. 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
    2. 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self。
    3. Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this关键字,但是有局限性。
    1.全局环境中,this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,ES6 模块中this返回的是undefined。
    2.函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。
    3.不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用

对象拓展

"注意事项"
	1.属性名表达式与简洁表示法,不能同时使用,会报错

属性的简洁表示法

ES6 允许在大括号里面,直接写入"变量"和"函数",作为对象的"属性"和"方法"。这样的书写更加简洁。
	'变量'
    const foo = 'bar';
    const baz = {foo};
    baz // {foo: "bar"}
    // 等同于
	const baz = {foo: foo};

    '函数'
    const o = {
      method() {
        return "Hello!";
      }
    };
    // 等同于
    const o = {
      method: function() {
        return "Hello!";
      }
    };

	'注意'
	简写的对象"方法"不能用作构造函数会报错,简写的对象"属性"的方法可以用作构造函数;总的来说就是"对象的简写方法不能用做构造函数,对象的属性(无论简写与否)对应的方法可以用做构造函数"
    const obj = { // 在对象简写方法上
      f() {
        this.foo = 'bar';
      }
    };
    new obj.f() // 报错

	————————————————————————分隔符

    var f =   function() { // 在对象属性(无论简写与否)
        this.foo = 'bar';
      }
    const obj = { f };
    // const obj = {
    //   f: function() {this.foo = 'bar'};
    // }
    let a = new obj.f()
    console.log(a.foo) // 'bar'

属性名表达式

ES6 允许将表达式(表达式含义详见目录"表达式")作为对象的属性名,即把表达式放在方括号内。 
    let propKey = 'foo';
    let obj = {
      [propKey]: true,
      ['a' + 'bc']: 123
    };
	obj['foo'] // true
	obj[propKey] // true
	obj['abc'] // 123
	——————————————————————————分隔符
    

	'注意'
	表达式作为属性名时均会自动的执行对应valueOf方法或其它方方式(例如函数执行后的返回值)得到值并且会再次调用toString方法转为字符串形式。
    '通过valueOf方法取得值'
    const keyA = {a: 1};
    const keyB = {b: 2};
    const myObject = {
        [keyA]: 'valueA',
        [keyB]: 'valueB'
    };
    console.log(myObject) // Object {[object Object]: "valueB"}
	————————————————————————分隔符
    var f =   function() {
        return 'foo'
      }
    const obj = { 
      [f]: 'kk'
     };
    console.log(obj[f]) // 'kk'

	'通过函数执行取得值'
        var f =   function() {
            return 'foo'
        }
        const obj = { 
            [f()]: 'kk'
        };
        console.log(obj.foo) // 'kk' 

	'通过属性访问表达式取得值'
		var a = ['foo']  
        const obj = { 
          [a[0]]: 'kk'
         };
        console.log(obj.foo) // 'kk'

	'...等等'

super关键字

ES6又新增了另一个类似的关键字super,指向当前对象的原型对象
'注意:super关键字只能用于对象的简写方法'

const proto = {
  foo: 'hello'
};
const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
————————————————————分隔符
// 报错
const obj = {
  foo: super.foo
}
// 报错
const obj = {
  foo: () => super.foo
}
// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

弱引用对象

弱引用对象(弱引用数据类型): 即垃圾回收机制不考虑"弱引用对象"对某对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于"弱引用对象"之中,就是说只要所引用的对象的其他引用都被清除(弱引用对象引用这个对象没影响),垃圾回收机制就会释放该对象所占用的内存。
"WeakSet" 详见《WeakSet》目录
"WeakMap" 详见《WeakMap》目录
"WeakRef" 用于直接创建对象的弱引用
    let target = {};
    let wr = new WeakRef(target);
    上面示例中,target是原始对象,构造函数WeakRef()创建了一个基于target的新对象wr。这里,wr就是一个 WeakRef 的实例,属于对target的弱引用,垃圾回收机制不会计入这个引用,也就是说,wr的引用不会妨碍原始对象target被垃圾回收机制清除。
    WeakRef 实例对象有一个'deref()方法',如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回undefined。
    let target = {};
    let wr = new WeakRef(target);
    let obj = wr.deref();
    if (obj) { // target 未被垃圾回收机制清除
      // ...
    }
    上面示例中,deref()方法可以判断原始对象是否已被清除。
    
    
"FinalizationRegistry"
清理器注册表功能 FinalizationRegistry,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。

首先,新建一个注册表实例。
const registry = new FinalizationRegistry(heldValue => {
  // 回调函数的参数heldValue可以是任意类型的值,字符串、数值、布尔值、对象,甚至可以是undefined。
  // ....
});
上面代码中,FinalizationRegistry()是系统提供的构造函数,返回一个清理器注册表实例,里面登记了所要执行的回调函数。回调函数作为FinalizationRegistry()的参数传入,它本身有一个参数heldValue。

然后,注册表实例的register()方法,用来注册所要观察的目标对象。
	registry.register(theObject, "some value");
上面示例中,theObject就是所要观察的目标对象,一旦该对象被垃圾回收机制清除,注册表就会在清除完成后,调用早前注册的回调函数,并将some value作为参数(前面的heldValue)传入回调函数。

最后,如果以后还想取消已经注册的回调函数,则要向register()传入第三个参数,作为标记值。这个标记值必须是对象,一般都用原始对象。接着,再使用注册表实例对象的unregister()方法取消注册。
    registry.register(theObject, "some value", theObject);
    // ...其他操作...
    registry.unregister(theObject);

字符串

字符串编码

"字符串的 Unicode 规则为\u + 四位十六进制,这种新的字符表示方式只能表示  \u 0000 ~ \u ffff 之间的数字。 如果超出范围必须用双字节表示"

1.如果不按照规则熟悉书写 例如 console.log("\uD842\uDFB69")      
     -这个9是多余字符; 那么则认为这段字符是 \uD842\uDFB6 + 9 所以打印结果是 9;
     -如果想要一次性表示超出范围的字符那么我们可以使用{}来表示,ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符
         -例如: 
             console.log("\u20BB9");  这个的打印结果是 拆分开来的 ₻9
             console.log("\u{20BB9}"); 这个打印的结果是一个完整的字符

字符与编码

JavaScript 字符串允许直接输入字符,以及输入字符的转义形式(编码形式)
'中' === '\u4e2d' // true

    JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式,其他的均可直接使用字符。
        U+005C:反斜杠(reverse solidus)
        U+000D:回车(carriage return)
        U+2028:行分隔符(line separator)
        U+2029:段分隔符(paragraph separator)
        U+000A:换行符(line feed)
		如果一定要直接使用的话,可以使用转义符号 \ 来使用,如: \\ 表示 字符'\', '\\' === '\u005c' // true 

字符串模板

模板字符串(template string)是增强版的字符串,用反引号(``)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。


"解析变量|表达式|函数调用"
    字符串模板可以把表达式放在${}里面,他就可以解析
    '基本类型'
    var num = 10
    console.log('我要打' + num + '个')
    console.log(`我要打${num}个`)
	
	'引用类型'
	// 会默认调用对象的toString()方法
	var obj = {a:2};
	console.log(`${ojb}`) // [object Object]
	var arr = [1,2];
	console.log(`${arr}`) // '1,2'

	'表达式'
    var str1 = `我要打${num >= 10 ? '很多' : '0'}个`
    console.log(str1)

	'函数'
    // ${}里面可以函数调用,会被解析成函数的返回值
    var str2 = `我要打${fn()}个`
    console.log(str2)


'多行字符串'
	// 所有的空格和缩进都会被保留在输出之中
    var str = `<p>我要打
                <b>${num}</b>
                个
               </p>`
    document.write(str)


'模板字符窜内还有`'
    // 如果字符串本身就有一个`,那么要写成\`,转义成`,防止出现歧义
    var str3 = `hello \` world`
    console.log(str3)

'Unicode编码'
	console.log('\u20BB') // ₻
	console.log(`\u20BB`) // ₻ 因为其模板字符串就是在原有的字符串模式下添加了新的操作方法

"模板字符串与模板字符串嵌套"
const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;
//注意只能这样嵌套 `${`${}`}`即在${}内部填写另外一个模板字符串
 `${`${'哈哈'}`}` // '哈哈'
 
上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,实战使用方法如下。
const data = [
    { first: '<Jane>', last: 'Bond' },
    { first: 'Lars', last: '<Croft>' },
];

console.log(tmpl(data));
// <table>
//
//   <tr><td><Jane></td></tr>
//   <tr><td>Bond</td></tr>
//
//   <tr><td>Lars</td></tr>
//   <tr><td><Croft></td></tr>
//
// </table>

标签模板

"模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)"

alert`hello`
// 等同于
alert(['hello'])
标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
    let a = 5;
    let b = 10;

    tag`Hello ${ a + b } world ${ a * b }`;
    // 等同于
    tag(['Hello ', ' world ', ''], 15, 50);

上面代码中,模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。函数tag依次会接收到多个参数。
    function tag(stringArr, value1, value2){
      // ...
    }
应用
'将各个参数按照原来的位置拼合回去'
    let total = 30;
    let msg = passthru`The total is ${total} (${total*1.05} with tax)`;

    function passthru(literals) {
      let result = '';
      let i = 0;

      while (i < literals.length) {
        result += literals[i++];
        if (i < arguments.length) {
          result += arguments[i];
        }
      }

      return result;
    }

    msg // "The total is 30 (31.5 with tax)"


'标签模板的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容'
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
  let s = templateData[0];
  for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);

    // Escape special characters in the substitution.
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // Don't escape special characters in the template.
    s += templateData[i];
  }
  return s;
}
上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。

let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

message
// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>


'标签模板的另一个应用,就是多语言转换(国际化处理)'
// ...

箭头函数

箭头函数this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为这个,所以箭头函数也不能做构造函数。

缺陷:
    1.箭头函数是不能new的,它的设计初衷就跟构造函数不太一样
    2.箭头函数如果要返回一个JSON对象,必须用小括号包起来 var test = ()=>({id:3, val=20})

扩展运算符 (...)

1.扩展运算符后面不是对象,则会自动将其转为对象
    // 等同于 {...Object(true)}
    {...true} // {}

    // 等同于 {...Object(undefined)}
    {...undefined} // {}

    // 等同于 {...Object(null)}
    {...null} // {}

    // 等同于 {...Object('hello')}
    {...'hello'} // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
2.扩展运算符(...)三个点号功能是把数组或类数组对象展开成一系列用逗号隔开的值
3.扩展运算符(...)用于取出参数"对象自身中的所有可遍历(可枚举)属性",拷贝到当前对象之中,扩展运算符(...)内部使用for...of循环
4.拓展运算符它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
5.扩展运算符对对象实例的拷贝属于一种浅拷贝 (自己:js中万物皆为对象)
6.拓展运算符和解构赋值结合使用时,拓展运算符必须是最后一个参数,否则会报错
7.扩展运算符后面还可以放置表达式

	'使用'
		1.扩展运算符后面还可以放置表达式
            const arr = [ ...(x > 0 ? ['a'] : []) ];
            const obj = {
                ...(x > 1 ? {a: 1} : {}),
                b: 2,
            };
		2.可以和函数参数结合使用(变相的变成rest运算符)
		
    注意,只有函数调用时,扩展运算符才可以放在圆括号中(变相的成了rest运算符),否则会报错。
        (...[1, 2]) // Uncaught SyntaxError: Unexpected number
        console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected number
        console.log(...[1, 2]) // 1 2

	'扩展运算符的应用'
        1.复制数组\对象
        2.合并数组\对象
        3.与解构赋值结合 [a, ...rest] = list; {...z} = {a: 1, b: 2}
        4.将字符串转为真正的数组 [...'hello']
        5.任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组
        
总结:可以说拓展运算符后跟这个要是一个值,也就是表达式

对象的扩展运算符

理解对象的扩展运算符其实很简单,只要记住一句话就可以:对象中的扩展运算符(...)用于取出参数对象自身中的所有可遍历属性,拷贝到当前对象之中

'对象的合并\复制'                                     
    let bar = { a: 1, b: 2 };
    let baz = { ...bar }; // { a: 1, b: 2 }
    // 同名属性会被覆盖掉。
    let bar = {a: 1, b: 2};
    let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}

'与解构赋值结合使用'
	let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }

"注意:这里有点需要注意的是扩展运算符对对象实例的拷贝属于一种浅拷贝。肯定有人要问什么是浅拷贝(就是并没有真正的变成另外一个值,还是同一个东西,因为浅拷贝的是地址)"

数组的扩展运算符

扩展运算符同样可以运用在对数组的操作中。

"可以将数组转换为参数序列"
    function add(x, y) {
      return x + y;
    }

    const numbers = [4, 38];
    add(...numbers) // 42

"可以复制数组"
	const arr1 = [1, 2];
    const arr2 = [...arr1];


"扩展运算符可以与解构赋值结合起来,用于生成数组"
    const [first, ...rest] = [1, 2, 3, 4, 5];
    first // 1
    rest  // [2, 3, 4, 5]

    需要注意的一点是:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

    const [...rest, last] = [1, 2, 3, 4, 5];
    // 报错
    const [first, ...rest, last] = [1, 2, 3, 4, 5];
    // 报错


"扩展运算符还可以将字符串转为真正的数组"
    [...'hello'] // [ "h", "e", "l", "l", "o" ]

"由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组"
    let foo = { ...['a', 'b', 'c'] };
    console.log(foo) // {0: "a", 1: "b", 2: "c"}

"任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组"
	比如:
        // arguments对象
        function foo() {
          const args = [...arguments];
        }

rest运算符(...变量)

rest运算符 '...变量' 可以把一系列逗号隔开的值合并到一个数组里,用来合并数组
    function fn (...arr) { // 可以把所有传进来的参数合并到arr
       console.log(arr)
    }
    fn(3,6,2,6,3)

解构赋值

1.解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其"转为对象"。例外:undefined和null不会转为对象
2.如果解构不成功,变量的值就等于undefined
3.解构赋值允许指定默认值
5.解构对象时会查找原型链(如果属性不在对象自身,将从原型链中查找)
    function A() {
      this.a = 3;
    }
    A.prototype.aa = 5;
    var obj = new A();
    var {aa, a} = obj;
    console.log(aa, a) // 5 3
4. "默认值" ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一解构的值等于undefined,默认值才会生效
5.数组的元素是按次序排列的,变量的取值由它的"位置"决定;而对象的属性没有次序,"键变量必须与属性同名",才能取到正确的值。
6."模式匹配"只要等号两边的模式相同或近似相同,左边的变量就会被赋予对应的值,解构分为"完全解构"和"不完全解构"。"完全解构"即等号左边的模式,匹配右边所有的等号右边的数组;"不完全解构"即等号左边的模式,只匹配一部分的等号右边的数组
7. ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式
	let { x, ...{ y, z } } = o; // 报错
8.只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值

数组

1.数组的元素是按次序排列的,变量的取值由它的"位置"决定

"完全解构"
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

"不完全解构"
let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4


"默认值"
    '默认值是确定的值'
    let [foo = true] = [];
    foo // true

    let [x, y = 'b'] = ['a']; // x='a', y='b'
    let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
    "注意": ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

    '默认值是表达式'
    function f() {
        console.log('aaa');
    }	
    let [x = f()] = [1]; 
    console.log(x); // 1 
    如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

    '默认值是其他变量'
    let [x = 1, y = x] = [];     // x=1; y=1
    let [x = 1, y = x] = [2];    // x=2; y=2
    let [x = 1, y = x] = [1, 2]; // x=1; y=2
    let [x = y, y = 1] = [];     // ReferenceError: y is not defined
    默认值可以引用解构赋值的其他变量,但该变量必须已经声明

对象

1.对象的属性没有次序,"键变量必须与属性同名",才能取到正确的值

'完全解构'
let { bar, foo, baz } = { foo: 'aaa', bar: 'bbb' };
// let { bar, foo, baz }相当于是let { bar: bar, foo: foo, baz: baz},ES6语法下同名情况下可以省略就成了let { bar, foo, baz }
foo // "aaa"
bar // "bbb"
baz // undefined

'不完全解构'
let { foo } = { foo: 'aaa', bar: 'bbb' };
foo // 'aaa'

'重命名取值'
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
// 对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
// 上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

	'引申'
	let obj = {};
    let arr = [];
    ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
    obj // {prop:123}
    arr // [true]
    ——————————————————分割
	let {foo: {bar}} = {baz: 'baz'}; // 报错
	// 左边foo没有对应到右边baz的名字,所以导致解构失败,所以foo: {bar}里的{bar}被复制成了undefined,这里就相当于成了{bar} = undefined,从undefined里取值就会直接报错

'重复取值'
let { foo, foo: foo2, foo: foo3 } = { foo: 'aaa', bar: 'bbb' };
foo  // 'aaa'
foo2 // 'aaa'
foo3 // 'aaa'

"取到继承的属性"
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"
// 可以取到继承的属性和原型链上自己设置的属性,取不到原型链原有的属性(即系统设定的属性)

'默认值'
var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

var {x = 3} = {x: undefined};
x // 3

var {x = 3} = {x: null};
x // null
'默认值是表达式'
'默认值是其他变量'
'引申'
    '数组本质是特殊的对象'
    let arr = [1, 2, 3];
    let {0 : first, [arr.length - 1] : last} = arr;
    first // 1
    last // 3

字符串

1.字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象(就是类数组对象)。
2.类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

数值和布尔值

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

函数参数

函数的参数也可以使用解构赋值。

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3
上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

下面是另一个例子。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值。

function move({x = 0, y = 0} = {}) {
  // 这种参数有默认值的首先会直接解构出默认值并直接赋值,之后函数执行时参数带入时再次进行解构赋值(关于函数参数设置默认值情况,详见目录"函数参数-设置默认值")
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

注意,下面的写法会得到不一样的结果。

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

undefined就会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

引申

let { 0: foo } = [1, 3];
console.log(foo); // 1

"解构赋值第二点的使用展示"
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

"解构赋值第七点的使用展示"
let obj = {
  p: {
    a: 'Hello',
    b: { ...{ y: 'World' } }
  }
};

let { p: {a:x,b:{y}} } = obj;
console.log(x,y);
// 上面的可以,下面的不行
let { x, ...{ y, z } } = { x: 1, y: 2, z: 3 };
console.log(x, y, z); // 报错

圆括号问题

1.解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。由此带来的问题是,如果模式中出现圆括号怎么处理。"ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号" 但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号

详情见: https://es6.ruanyifeng.com/#docs/destructuring#圆括号问题

用途

"交换变量的值"
let x = 1;
let y = 2
[x, y] = [y, x];

"从函数返回多个值"
// 返回一个数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

"函数参数的使用"
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
                       
"提取 JSON 数据"
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number); // 42, "OK", [867, 5309]
                       
"函数参数的默认值"
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

"遍历Map结构"
任何部署了 Iterator 接口的对象,都可以用for...of循环遍历(详情见目录Iterator)
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}
                       
"解构模块的方法"
const { SourceMapConsumer, SourceNode } = require("source-map");

#!

#!命令,写在脚本文件或者模块文件的第一行。

// 写在脚本文件第一行
#!/usr/bin/env node
'use strict';
console.log(1);

// 写在模块文件第一行
#!/usr/bin/env node
export {};
console.log(1);
有了这一行以后,Unix 命令行就可以直接执行脚本。

# 以前执行脚本的方式
$ node hello.js

# hashbang 的方式
$ ./hello.js
对于 JavaScript 引擎来说,会把#!理解成注释,忽略掉这一行。

Symbol

"原因":ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

1.Symbol函数会生成一个唯一的值可以理解为Symbol类型跟字符串是接近的,但每次生成唯一的值,也就是每次都不相等,至于它等于多少,并不重要 这对于一些字典变量,比较有用。

"不可以"
    1.Symbol()函数前不能使用new命令,否则会报错
    2.Symbol 值不能与其他类型的值进行运算,会报错
	3.Symbol 值作为对象属性名时,不能用点运算符,只能放在中括号内当变量来使用
    
    第"3"点示例
        const mySymbol = Symbol();
        const a = {};
        a.mySymbol = 'Hello!';
        a[mySymbol] // undefined
        a['mySymbol'] // "Hello!"
		不了解可以看目录"点运算符也就是取值运算符"

 "可以"
	1.Symbol()函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,不管参数是什么都会先转换为字符串。
    2.Symbol 值可以显式转为字符串
    3.Symbol 值也可以转为布尔值
    4.在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。同理,在对象外部取值,Symbol 值必须放在方括号之中
    
    '第2、3点详解'
        let s1 = Symbol('s1')
        String(s1) or s1.toString() // "Symbol(s1)"
        Boolean(s1)  // true

"description"

"消除魔术字符串"
	魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。如果该值等于哪个值并不重要,只要是为了不会跟其他属性的值冲突,就很适合改用 Symbol 值。

"Symbol.for(),Symbol.keyFor()"
	1.Symbol.for()方法得到的是相同的Symbol值。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
	2.Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
    3.Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key
        let s1 = Symbol.for("foo");
        let s2 = Symbol("foo");
        Symbol.keyFor(s1) // "foo"
        Symbol.keyFor(s2) // undefined
	4.Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。
        function foo() {
          return Symbol.for('bar');
        }
        const x = foo();
        const y = Symbol.for('bar');
        console.log(x === y); // true

内置的Symbol值

总结:
	1.Symbol对象上的方法有:
        observable
        asyncIterator
        for()
        hasInstance
        isConcatSpreadable
        iterator
        keyFor()
        length
        match
        matchAll
        name
        prototype
        replace
        search
        species
        split
        toPrimitive
        toStringTag
        unscopables

"Symbol.hasInstance" 使用instanceof运算符时自动调用此方法
	1.指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
    
        class MyClass {
            [Symbol.hasInstance](foo) {
                return foo instanceof Array;
            }
        }
        [1, 2, 3] instanceof new MyClass() // true

        ——————————————————分割线

        class Even {
            static [Symbol.hasInstance](obj) {
                return Number(obj) % 2 === 0;
            }
        }

        // 等同于
        const Even = {
            [Symbol.hasInstance](obj) {
                return Number(obj) % 2 === 0;
            }
        };

        1 instanceof Even // false
        2 instanceof Even // true
        12345 instanceof Even // false

"Symbol.isConcatSpreadable" 使用Array.prototype.concat()时自动调用此方法
	1.对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
    	'数组'
        let arr1 = ['c', 'd'];
        ['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
        arr1[Symbol.isConcatSpreadable] // undefined
        let arr2 = ['c', 'd'];
        arr2[Symbol.isConcatSpreadable] = false;
        ['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']

		'对象'
        对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true,才可以展开。
        let obj = {length: 2, 0: 'c', 1: 'd'};
        ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
        obj[Symbol.isConcatSpreadable] = true;
        ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']

"Symbol.species" 使用instanceof运算符时自动调用此方法
	1.对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。
	2.Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数
	
        class MyArray extends Array {
        }
        const a = new MyArray(1, 2, 3);
        const b = a.map(x => x);
        const c = a.filter(x => x > 1);
        b instanceof MyArray // true
        c instanceof MyArray // true
        上面代码中,子类MyArray继承了父类Array,a是MyArray的实例,b和c是a的衍生对象。你可能会认为,b和c都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

        
        Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为MyArray设置Symbol.species属性。
        class MyArray extends Array {
          static get [Symbol.species]() { return Array; }
        }
        上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get取值器。默认的Symbol.species属性等同于下面的写法。
        static get [Symbol.species]() {
          return this;
        }


        现在,再来看前面的例子。
        class MyArray extends Array {
          static get [Symbol.species]() { return Array; }
        }
        const a = new MyArray();
        const b = a.map(x => x);
        b instanceof MyArray // false
        b instanceof Array // true
        上面代码中,a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

        
        再看一个例子。
        class T1 extends Promise {
        }
        class T2 extends Promise {
          static get [Symbol.species]() {
            return Promise;
          }
        }
        new T1(r => r()).then(v => v) instanceof T1 // true
        new T2(r => r()).then(v => v) instanceof T2 // false
        上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。

"Symbol.toPrimitive" 该对象转化为原始类型时自动调用此方法
    1.Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
        Number:该场合需要转成数值
        String:该场合需要转成字符串
        Default:该场合可以转成数值,也可以转成字符串
    
        let obj = {
          [Symbol.toPrimitive](hint) {
            switch (hint) {
              case 'number':
                return 123;
              case 'string':
                return 'str';
              case 'default':
                return 'default';
              default:
                throw new Error();
             }
           }
        };

        2 * obj // 246
        3 + obj // '3default'
        obj == 'default' // true
        String(obj) // 'str'

"Symbol.toStringTag" 该对象转为字符串时自动调用此方法
	1.这个属性可以用来定制[object Object]或[object Array]中object后面的那个大写字符串。
        // 例一
            ({[Symbol.toStringTag]: 'Foo'}.toString())
        // "[object Foo]"

        // 例二
        class Collection {
            get [Symbol.toStringTag]() {
                return 'xxx';
            }
        }
        let x = new Collection();
        Object.prototype.toString.call(x) // "[object xxx]"

        // 例三
        let a = {}
        Object.defineProperty(a, Symbol.toStringTag, {
            get() {
                return 'xxx';
            }
        }
        )
        console.log(a.toString
                    
        ES6 新增内置对象的Symbol.toStringTag属性值如下。
        JSON[Symbol.toStringTag]:'JSON'
        Math[Symbol.toStringTag]:'Math'
        Module 对象M[Symbol.toStringTag]:'Module'
        ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
        DataView.prototype[Symbol.toStringTag]:'DataView'
        Map.prototype[Symbol.toStringTag]:'Map'
        Promise.prototype[Symbol.toStringTag]:'Promise'
        Set.prototype[Symbol.toStringTag]:'Set'
        %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
        WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
        WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
        %MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
        %SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
        %StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
        Symbol.prototype[Symbol.toStringTag]:'Symbol'
        Generator.prototype[Symbol.toStringTag]:'Generator'
        GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'

"Symbol.unscopables" 该对象指定了使用with关键字时自动调用此方法
	1.对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
        Array.prototype[Symbol.unscopables]
        // 打印结果
        // {
        //   copyWithin: true,
        //   entries: true,
        //   fill: true,
        //   find: true,
        //   findIndex: true,
        //   includes: true,
        //   keys: true
        // }

        Object.keys(Array.prototype[Symbol.unscopables])
        // ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
        上面代码说明,数组有 7 个属性,会被with命令排除。

        // 没有 unscopables 时
        class MyClass {
          foo() { return 1; }
        }
        var foo = function () { return 2; };
        with (MyClass.prototype) {
          foo(); // 1
        }

        // 有 unscopables 时
        class MyClass {
          foo() { return 1; }
          get [Symbol.unscopables]() {
            return { foo: true };
          }
        }
        var foo = function () { return 2; };
        with (MyClass.prototype) {
          foo(); // 2
        }
        上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

Array.from()

1. Array.from() 方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
2. Array.from() 静态方法从可迭代或类数组对象创建一个新的浅拷贝的数组实例。
3. 要将一个类数组对象转换为一个真正的数组,必须具备以下条件:
    1. 该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。
    2. 该类数组对象的属性名必须为数值型或字符串型的数字


"将类数组对象转换为真正数组"
    let arrayLike = {
        0: 'tom', 
        1: '65',
        2: '男',
        3: ['jane','john','Mary'],
        'length': 4
    }
    let arr = Array.from(arrayLike)
    console.log(arr) // ['tom','65','男',['jane','john','Mary']]
	// 如果将上面代码中length属性去掉呢?实践证明,答案会是一个长度为0的空数组。


"将Set结构的数据转换为真正的数组”
    let arr = [12,45,97,9797,564,134,45642]
    let set = new Set(arr)
    console.log(Array.from(set))  // [ 12, 45, 97, 9797, 564, 134, 45642 ]


"Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组"
    let arr = [12,45,97,9797,564,134,45642]
    let set = new Set(arr)
    console.log(Array.from(set, item => item + 1)) // [ 13, 46, 98, 9798, 565, 135, 45643 ]


"将字符串转换为数组"
    let  str = 'hello world!';
    console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]


"Array.from参数是一个真正的数组"
    console.log(Array.from([12,45,47,56,213,4654,154]))
    // 像这种情况,Array.from会返回一个一模一样的新数组

知识借鉴:https://www.cnblogs.com/jf-67/p/8440758.html

Set&Map

1. map和set都是可迭代的对象,map是"值-值"存在的,set是只有"值"存在的,两者都可以通过Array.from转变成数组
2. Map保存"键值对"能够记住键的原始插入顺序; set保存"值"时也能够记住值的原始插入顺序
3. map相当于是"以值代替键"的对象,是一种"值-值"对存在的对象,非"键-值"对存在的对象
4. Map和Set中对重复值的判断基本遵循严格相等===的判断,对于NaN === NaN 在Map和Set中均被认定是true; Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
5.keys方法、values方法、entries方法返回的都是遍历器对象

Set

1.Set并不是数组,但类似数组的结构,无重复值,无键只有值
2.Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化
3.Set函数add的成员可以是任意类型
    // 例一
    const set = new Set([1, 2, 3, 4, 4]);
    [...set]
    // [1, 2, 3, 4]

    // 例二
    const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    items.size // 5

    // 例三
    const set = new Set(document.querySelectorAll('div'));
    set.size // 56

"Set结构的实例有以下属性":
    Set.prototype.constructor:构造函数,默认就是Set函数。
    Set.prototype.size:返回Set实例的成员总数。

"Set实例的方法分为两大类":操作方法(用于操作数据)和遍历方法(用于遍历成员)
	'操作方法'
        Set.prototype.add(value):添加某个值,返回 Set 结构本身。
        Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
        Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
        Set.prototype.clear():清除所有成员,没有返回值。
	'遍历方法'
        Set.prototype.keys():返回键名的遍历器
        Set.prototype.values():返回键值的遍历器
        Set.prototype.entries():返回键值对的遍历器
        Set.prototype.forEach():使用回调函数遍历每个成员
        "注:"Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。


'size' 成员总数
    const arr = [1,2,2,3,4,5,5,6]
    const set = new Set(arr);
    const setArr = Array.from(set)
    console.log('原数组长度',arr.length) // 8
    console.log('set的成员总数',set.size) // 6
    console.log('set转数组之后的长度',setArr.length) // 6


'add()' 添加某个值,返回 Set 结构本身
    const arr = [1,2,2,3,4,5,5,6]
    const set = new Set(arr);
    set.add('hello').add(2) // 可链式添加
    console.log(set) // {1, 2, 3, 4, 5, 6, 'hello'}

'delete()' 删除某个值,返回一个布尔值,表示删除是否成功
    const arr = [1,2,2,3,4,5,5,6]
    const set = new Set(arr);
    console.log(set) // [1, 2, 3, 4, 5, 6]
    console.log(set.delete(2)) // [1, 3, 4, 5, 6]

'has(value)' 返回一个布尔值,表示该值是否为Set的成员
    const arr = [1,2,2,3,4,5,5,6]
    const set = new Set(arr);
    console.log(set.has(2)) // true
    console.log(set.delete(2)) // true
    console.log(set.has(2)) // false
    console.log(set) // [1, 3, 4, 5, 6]

'clear()'' 清除所有成员,没有返回值
    const arr = [1,2,2,3,4,5,5,6]
    const set = new Set(arr);
    console.log(set.clear())
    console.log(set.delete(2)) // false
    console.log(set.add(2)) // [2]
    console.log(set.has(2)) // true
    console.log(set) // [2]

'entries()' 返回键值对的遍历器
    const set1 = new Set();
    set1.add(42);
    set1.add('forty two');
    const iterator1 = set1.entries();
    console.log(iterator1) // SetIterator {42 => 42, 'forty two' => 'forty two'}
    for (const entry of iterator1) {
        console.log(entry);
        // Expected output: Array [42, 42]
        // Expected output: Array ["forty two", "forty two"]
    }

'keys()' 方法完全等价于 values() 方法。
	// 由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
    const mySet = new Set();
    mySet.add("foo");
    mySet.add("bar");
    mySet.add("baz");

    const setIter = mySet.keys();

    console.log(setIter.next().value); // "foo"
    console.log(setIter.next().value); // "bar"
    console.log(setIter.next().value); // "baz"

'values()' 该对象包含此集合对象中每个元素的值,按插入顺序排列。
    const set1 = new Set();
    set1.add(42);
    set1.add('forty two');
    const iterator1 = set1.values();
	console.log(iterator1) // SetIterator {42, 'forty two'}
 	for (const entry of iterator1) {
        console.log(entry,1);
        // Expected output: 42
        // Expected output: forty two
    }
    // console.log(iterator1.next().value); // 42
    // console.log(iterator1.next().value); // "forty two"

'forEach()'
	'只有一个参数'
    function logSetElements(value1, key, set) {
      // 该函数的参数与数组的forEach一致,依次为键值、键名、集合本身
      console.log(`s[${value1}] = ${key}`);
    }
    new Set(['foo', 'bar', undefined]).forEach(logSetElements);
    // Expected output: "s[foo] = foo"
    // Expected output: "s[bar] = bar"
    // Expected output: "s[undefined] = undefined"

	'有第二个参数'
	// 第二个参数,表示绑定处理函数内部的this对象
    new Set(['foo', 'bar', undefined]).forEach((value, key, set)=>{
        console.log(this) // #document
    }, document);


"Set在使用过程中需要注意的地方"
"无重复值"
    const set = new Set([1, 2, 3, 4, 4]);
    console.log([...set]) // [1, 2, 3, 4]
  
"两个对象总不相等(如果放入同样地址的数据会被去重,这里是因为下面的对象地址不同)"
    const set = new Set([{}, {},{name:'shyno'},{name:'shyno'}]);
    console.log(...set)

"无键只有值"
    const arr = [1,2,2,3,4,5,5,6]
    let obj = {name:'shyno'}
    const set = new Set(arr);
    const setArr = Array.from(set)
    console.log(Object.keys(arr)) // ['0', '1', '2', '3', '4', '5', '6', '7'] 
    console.log(Object.keys(obj)) // ['name'] 
    console.log(Object.keys(set)) // []

"Set并不是数组"
    const set = new Set([{}, {},{name:'shyno'},{name:'shyno'}]);
    document.write(set[0])         // undefined
    console.log(set.concat([1,2])) // set.concat is not a function 

"set中NaN等于NaN,其余比较相当于 ==="
    let set3 = new Set();
    let a = NaN;
    let b = NaN;
    set3.add(a);
    set3.add(b);
    console.log(set3)

'遍历器'
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
	Set.prototype[Symbol.iterator] === Set.prototype.values
	// true
这意味着,可以省略values方法,直接用for...of循环遍历 Set。
    let set = new Set(['red', 'green', 'blue']);
    for (let x of set) {
      console.log(x);
    }
    // red
    // green
    // blue

'应用'
// 去除数组的重复成员
[...new Set(array)]
Array.from(new Set(array));

//去除字符串里面的重复字符。
[...new Set('ababbc')].join('') // "abc"

知识借鉴:
    set的应用:https://www.zhihu.com/tardis/bd/art/457005328?source_id=1001
    set的应用:https://www.cnblogs.com/wjcoding/p/11690886.html
    set的介绍:https://www.cnblogs.com/Shyno/p/12167915.html

WeakSet

1.WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中,就是说只要所引用的对象的其他引用都被清除(弱引用对象引用这个对象没影响),垃圾回收机制就会释放该对象所占用的内存。这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
2.ES6 规定 WeakSet 不可遍历。
3.WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象和 Symbol 值,而不能是其他类型的值,如接受一个数组其数组内的成员也必须是对象和Symbol值才行。add的成员也只能是对象和Symbol值
4.WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)

"WeakSet 结构有以下三个方法"
    WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员,返回 WeakSet 结构本身。
    WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员,清除成功返回true,如果在 WeakSet 中找不到该成员或该成员不是对象,返回false。
    WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

Map

1. map是可迭代的对象,是"值-值"存在的
2. map是ES提供的一种字典数据结构。字典结构——用来存储不重复key的hash结构。不同于集合(set)的是,字典使用的是键值对的形式来存储数据
3.map接受数组作且每个成员都是一个双元素的数组的数据结构和任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数
4.Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键,虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

"Map的使用场景"
    1.只需要键值对的结构时,即 key => value 的结构
    2需要字符串以外的键或者值, 如DOM节点作为对象data的键

JavaScript对象(object:{})只能用字符串来当key,这对使用带来了不便,为了解决这个问题,ES6提供了map数据结构。其类似于对象,也是键值对的集合,但“key”的范围不仅限于字符串,而是各种类型的值都可以当做key。也就是说,object提供了“字符串-值”的对应结构,map则提供的是“值-值”的对应。是一种更加完善的hash结构, 如果你需要“键值对”的数据结构,Map 比 Object 更合适。

对象对map结构的支持演示
    var data1={a:1},
        data2={b:2},
        obj={};
    //为obj对象添加属性(将data1和data2作为属性名)
    obj[data1]=1;
    obj[data2]=2;
    console.log(obj); // {[object Object]: 2}
	// 打印结构并非是我们想要的,所以使用Map数据结构

"Map结构的实例有以下属性":
    Map.prototype.constructor:构造函数,默认就是Map函数。
    Map.prototype.size:返回Map实例的成员总数。

"Map实例的方法分为两大类":操作方法(用于操作数据)和遍历方法(用于遍历成员)
	'操作方法'
        Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
        Map.prototype.get(value):get方法读取key对应的键值,如果找不到key,返回undefined。
        Map.prototype.delete(value):删除某个键,如果删除成功返回true,否则返回false
        Map.prototype.has(value):判断某个键是否存在于map中,返回布尔值
        Map.prototype.clear():清除所有成员,没有返回值。
	'遍历方法'
        Map.prototype.keys():返回键名的遍历器
        Map.prototype.values():返回键值的遍历器
        Map.prototype.entries():返回键值对的遍历器
        Map.prototype.forEach():使用回调函数遍历每个成员
        "注:"Map的遍历顺序就是插入顺序。


"使用map结构"
	// 注意,要传递的是二维数组,因为二维数组才能体现出键值对
    const map=new Map([
        [{a:1},1],
        [{b:2},2]
    ]);
    console.log(map); // {{…} => 1, {…} => 2}

    // Map构造函数接受数组作为参数,实际上执行的是下面的算法。
    const items = [
      ['name', '张三'],
      ['title', 'Author']
    ];
    const map = new Map();
    items.forEach(
      ([key, value]) => map.set(key, value)
    );

'size' : map.siz 用于获取map的长度
	console.log(map.size);

'set()' : Map.set(key,value)  用来设置键值
	// 设置键名key对应的键值为value,然后返回整个map结构。如果key已经有值,则键值会被更新,否则生成新的键 
	const map=new Map([
        [{a:1},1],
        [{b:2},2]
    ]);
    let arr = [1,2,3]
    console.log(map.set(arr,'3元素的数组'));
    //也可以链式添加
    map.set('name','张三').set('age',28);
    console.log(map);

'get()' : Map.get(key) 读取键对应的值,如果获取不到则返回undefined
    const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
    console.log(map.get('name'));
   
'delete()' : Map.delete(key)  删除某个键,如果删除成功返回true,否则返回false
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
	console.log(map.delete('name'));

'has()' : Map.has(key)  判断某个键是否存在于map中,返回布尔值
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
	console.log(map.has('age'));

'clear()' : Map.clear()  清除所有数据,没有返回值
    const map=new Map([
            ['name',1],
            [{b:2},2]
        ]);	
    map.clear();

'keys()' : Map.keys()  返回键名的遍历器
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
	console.log(map.keys()); // MapIterator {'name', {…}}

'values()' : Map.values()  返回键值的遍历器
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
	console.log(map.values()); // MapIterator {1, 2}

'entries()' : Map.entries()  返回键值对的遍历器
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
	console.log(map.entries()); // MapIterator {'name' => 1, {…} => 2}

'forEach()' : map.forEach(回调函数, 回调函数的指向)   用来遍历 Map 的成员它有两个参数,第一个参数为回调函数,第二个参数设定回调函数中this指向什么
	'只有一个参数'
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
    map.forEach(function (value, key, map) {
        console.log(`这是key:${key},这是value:${value},这是map本身:${map}`);
    })

	'有第二个参数'
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
    map.forEach(function(value, key, map) {
        console.log(this); // #document
    }, document);
	


"Map在使用过程中需要注意的地方"
1.在js中NaN是不等于自身的一种数据类型,但是在map中,多次set以NaN为key的数据的时候,会产生覆盖的行为。说明map会将NaN视为同一个键
	const map=new Map([
        ['name',1],
        [{b:2},2]
    ]);
    map.set(NaN,1).set(NaN,10);
    console.log(map);

2.如果map数据中以空对象作为key({})的话,会发现,出现了两个key,这是因为每一个{}都是全新的引用, 其不同的是内存地址。而反观上面的NaN,其地址只有一个,所以才会出现覆盖的情况
	// 例一
    map.set({},1).set({},2);
    console.log(map);

	// 例二
    只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
        const map = new Map();
        map.set(['a'], 555);
        map.get(['a']) // undefined
        上面代码的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

3.放入同样的数据输出时地址不会相等
	// 例一
    const m1 = new Map([
        ['watermelon', 'fruit'],
        ['cat', 'animal']
    ])
    const m2 = new Map(m1); // 这里相当于把m1复制过去,给了m2,不过它们不是同一个 Map
    console.log(m2);
	console.log(m2 === m1); // false

	// 例二
    var arr = [
        ['watermelon', 'fruit'],
        ['cat', 'animal']
    ]
    const m1 = new Map(arr)
    const m2 = new Map(arr); 
    console.log(m2 === m1); false

	// 例三
    const map = new Map();
    const k1 = ['a'];
    const k2 = ['a'];
    map.set(k1, 111).set(k2, 222);
    map.get(k1) // 111
    map.get(k2) // 222

'遍历器'
Map 结构的实例默认可遍历,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
	map[Symbol.iterator] === map.entries // true

'应用'
// Map转为数组、数组转为Map、Map 转为对象、对象转为 Map、Map 转为 JSON、JSON 转为 Map
// Map转为数组
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。
const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
[...map.keys()] // [1, 2, 3]
[...map.values()] // ['one', 'two', 'three']
[...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']]
[...map] // [[1,'one'], [2, 'two'], [3, 'three']]

// Map转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}
const myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap) // { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

// 对象转为Map
对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

// Map转为JSON
Map 转为 JSON 要区分两种情况。一种情况是,"Map 的键名都是字符串",这时可以选择转为对象 JSON。
function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap) // '{"yes":true,"no":false}'

另一种情况是,"Map 的键名有非字符串",这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]'

// JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']}


知识借鉴:https://www.cnblogs.com/sky903700252/p/8798309.html
		https://blog.csdn.net/m0_51573433/article/details/122396493

总结:size has get set keys values entries delete forEach  clear

WeakMap

1.WeakMap 中的对象都是弱引用,即垃圾回收机制不考虑 WeakMap 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakMap 之中,就是说只要所引用的对象的其他引用都被清除(弱引用对象引用这个对象没影响),垃圾回收机制就会释放该对象所占用的内存。这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakMap 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakMap 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakMap 里面的引用就会自动消失。
2.ES6 规定 WeakMap 不可遍历。
3.WeakMap只接受对象(null除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。set的键名也是一样

"注意",WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
    const wm = new WeakMap();
    let key = {};
    let obj = {foo: 1};
    wm.set(key, obj);
    obj = null;
    wm.get(key) // Object {foo: 1}
    上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。
    
"WeakMap 结构有以下四个方法"
    Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
    Map.prototype.get(value):get方法读取key对应的键值,如果找不到key,返回undefined。
    Map.prototype.delete(value):删除某个键,如果删除成功返回true,否则返回false
    Map.prototype.has(value):判断某个键是否存在于map中,返回布尔值

构造函数

构造函数:用来在创建对象时初始化对象。
特点:构造函数名一般为大写字母开头;与new运算符一起使用来实例化对象。

用来构造(创建)对象的函数
    他在声明的时候跟普通函数没有区别
    用new运算符加上函数的调用,调用的结果就是一个对象
    构造函数中的this指的是即将要new的那个对象
     
当我们通过new运算符来构造函数的时候到底发生了什么,详细见下方'new操作符'目录(第一步和第三步咱们是看不见的)
    1. 构造函数里会创建一个空对象,然后把this指向这个对象
    2. 执行构造函数里的代码,给this对象新增属性
    3. 会把这个对象return出来
    
    
举例:
    function Person(){} // Person构造函数
    var p=new Person(); // Person构造函数创建对象,也可叫做实例化


<script>
    // 构造函数:用来构造一系列结构相同的对象的函数
    // 类是对象的原型,对象是类的实例
    // js里没有类,那么就是用构造函数来充当类

    function Fn () {}

    // 普通函数和构造函数的区别在于调用,可以用来new的就是构造函数
    // 一般构造函数首字母习惯大写
    var a = fn()
    var b = new Fn()
    console.log(a) // undefined
    // 称b是Fn的实例,Fn是b的原型
    console.log(b) // 空对象
    console.log(typeof b) // object
  </script>

new操作符

因为在使用new关键字对构造函数进行实例化的时候,在最后它会自动的执行,所以在使用new关键字的可以发现。
    let mynew_2 = new MyNew_1 // 不推荐
    let mynew_3 = new MyNew_1() // 推荐
	上述的两种的方式虽然都可以,但是更推荐使用第二种,相较于第一种来说,更容易被识别。
    
'new之后发生了什么'
    1.创建一个空对象,作为将要返回的对象实例。
    2.将这个空对象的原型,指向构造函数的prototype属性。
    3.将这个空对象赋值给函数内部的this关键字。
    4.开始执行构造函数内部的代码。

'new.target'
当我们使用 new 操作符调用构造函数时,new.target 属性的值为构造函数,否则为 undefined
    function Person (fullName) {
            if (typeof new.target !== 'undefined') {
                this.fullName = fullName;
            } else {
                throw new Error('须通过 new 关键字来调用 Person。');
            }
        }
    let student = new Person('aa');

知识借鉴:https://blog.csdn.net/aglorice/article/details/129473608

构造实例

 function Person (name, age) {
      // 构造函数里的this指的是将来new的那个实例
      // this是将来的实例,给实例新增一个name属性,属性值为参数name
      this.name = name
      this.age = age
     
      // 给对象新增方法
      this.say = function () {
        console.log(`My name is ${this.name}`)
      }
    }

    var xm = new Person('xiaoming', 18)
    console.log(xm)
    xm.say()

    var xh = new Person('xiaohong', 20)
    console.log(xh)
    xh.say()

    // 虽然say方法体是一样的,但是每一个实例里有一个自己的say,地址是不一样的
    console.log(xm.say === xh.say) // false

原型

- prototype是函数特有的属性, 是Function的静态属性;__proto__是对象特有的属性。
- 每个函数都有一个prototype属性, 它默认最终指向null
- 原型对象中有一个属性constructor, 它指向函数对象即函数。
- 因为函数本身是一种对象,所以函数既有prototype属性也有proto属性。
      当函数使用prototype属性时,是作为构造函数使用;
      当函数使用proto属性时,是作为一个对象使用。
- 给原型对象添加方法直接使用:Fun.prototype.方法名 = function() {方法体}
 	 作用:函数的所有实例对象自动拥有原型中的属性(方法)
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
  	对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- 原型链属性问题 
      1. 读取对象的属性值时: 会自动到原型链中查找
      2. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
      3. 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上

image-20210215144106740

原型上添加方法
<script>
  function Cfb(n) {
    this.n = n
    this.str = ''
    this.init()
    this.print()
  }

  // 对象合并,把新增的方法合并到prototype上
  Object.assign(Cfb.prototype, {
    // ES6对象增强写法,跟写键值对是一样的
    // init () {} 和 init: function () {} 是一样的
    init() {
      for (var i = 1; i <= this.n; i++) {
        for (var j = 1; j <= i; j++) {
          this.str += `${j} x ${i} = ${i * j}&nbsp;`
        }
        this.str += '<br>'
      }
    },
    print() {
      document.write(this.str)
    }
  })

  new Cfb(9)
  new Cfb(19)
</script>
1. 我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
2. prototype(原型)属性指向的对象就是原型对象
3. js中万物皆对象,因此原型也是对象,可以通过原型实现对象属性的继承

4. 几个必记的属性和方法
        - constructor  prototype里面的constructor指向当前对象的构造函数
        - __proto__  === [[prototype]]  指向类的prototype
        - prototype   指向当前对象的原型对象
        - instanceof   运算符,判断当前对象是否是另一个对象的实例
        - hasOwnProperty 判断对象上是否存在某个属性,并且这个方法会过滤到原型上的属性
        - isPrototypeOf   检查一个对象是否存在于另一个对象的原型链上
原型链
"原型链:一个对象在调用方法或者访问属性的时候沿着原型一层一层往上查找的链式结构"  
<script>
    function Dog (name) {
      this.name = name

    }
    Dog.prototype.say = function () {
      console.log(`My name is ${this.name}`)
    }

    var snoopy = new Dog('Snoopy')
    snoopy.say()
    console.log(snoopy)

    // 实例对象身上有一个__proto__ 属性,这个属性是一个指针,指向构造函数的原型对象
    console.log(snoopy.__proto__ === Dog.prototype) // true

    // Dog.prototype 是一个普通对象,所以他的构造函数是Object
    console.log(Dog.prototype)

    // Dog.prototype作为普通对象来看待,那么他的__proto__ 指向Object.prototype
    console.log(Dog.prototype.__proto__ === Object.prototype) // true

    console.log(Object.prototype.__proto__) // null

    console.log(Object.prototype)
    
    // 当snoopy调用toString方法的时候先找自己,自己没有沿着原型链一层一层网上找,在Object.prototype找到了这个方法然后就调用,如果Object.prototype也没有,那就报错
    console.log(snoopy.toString())

    // 实例对象的__proto__ 指向构造函数的prototype
    // snoopy是实例,Dog是他的构造函数
    // Dog.prototype是实例,他的构造函数是Object
  </script>
constructor
  <script>
    function Dog (name) {
      this.name = name
    }
    Dog.prototype.say = function () {
      console.log(`My name is ${this.name}`)
    }
    new Dog()
    console.dir(Dog)

    // 构造函数的原型对象上有一个constructor属性,这个属性指回构造函数本身
    // constructor在项目中一般不会刻意去使用,但是你得知道有这个东西
    console.log(Dog.prototype.constructor === Dog) // true
  </script>
instanceof
 <script>
    function Dog (name) {
      this.name = name
    }
    Dog.prototype.say = function () {
      console.log(`My name is ${this.name}`)
    }
    var snoopy = new Dog('Snoopy')

    console.log(snoopy instanceof Dog) // true
    console.log(snoopy instanceof Object) // true

    // js里的任何数据都是Object的实例,任意数据使用instanceof去验证Object都会得到true
    // js中的任意数据都可以沿着原型链找到Object.prototype,因此我们说js里一切皆为对象

    // 但是js的面向对象跟传统的严格面向对象是有区别的,所以可以说js的数据都不是对象
    // 因为js本身是没有类的,js的面向对象都是基于原型来实现的


    // instanceof可以用来验证数组
    var arr = [3,6,2,5]
    var obj = { name: 'lisi'}
    console.log(typeof arr) // object
    console.log(typeof obj) // object

    console.log(arr instanceof Array) // true
    console.log(obj instanceof Array) // false
  </script>
hasOwnProperty
hasOwnProperty是Object.prototype的一个方法,它可是个好东西,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。

<script>
    function Dog (name) {
      this.name = name
    }
    Dog.prototype.say = function () {
      console.log(`My name is ${this.name}`)
    }
    var snoopy = new Dog('Snoopy')

    console.log(snoopy.hasOwnProperty('name')) // true 属于对象本身的属性得到true
    console.log(snoopy.hasOwnProperty('say')) // false 原型链上的得到false
    console.log(snoopy.hasOwnProperty('abc')) // false 压根就没有的也得到false
  </script>
isPrototypeOf
<script>
    function Dog (name) {
      this.name = name
    }
    Dog.prototype.say = function () {
      console.log(`My name is ${this.name}`)
    }
    var snoopy = new Dog('Snoopy')

    // 验证Dog.prototype是否在snoopy的原型链上
    console.log(Dog.prototype.isPrototypeOf(snoopy)) // true
    console.log(Object.prototype.isPrototypeOf(snoopy)) // true
    
  </script>

构造函数和原型链接

 <script>
    function Duck (name) {
      this.name = name
    }

    // 方法一般放在原型上(公共的位置,每一个实例都可以来调用)
    Duck.prototype.walk = function () {
      console.log(`${this.name} is walking`)
    }

    // 如果原型上要写多个方法,可以使用Object.assign
    var tang = new Duck('Tang')
    console.log(tang)
    tang.walk()

    // 实例对象的__proto__ 指向构造函数的prototype
    console.log(tang.__proto__ === Duck.prototype)

    // 把Duck.prototype看作一个整体,他就是一个普通对象,他的构造函数是Object
    console.log(Duck.prototype.__proto__ === Object.prototype)
    console.log(Object.prototype.__proto__) // null
  </script>

Proxy

1.Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
2.Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

'用法'
    var proxy = new Proxy(target, handler);
        Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。
        target参数表示所要拦截的目标对象。
        handler参数也是一个对象,用来定制拦截行为。
     '示例'
        var obj = new Proxy({}, {
          get: function (target, propKey, receiver) {
            console.log(`getting ${propKey}!`);
            return Reflect.get(target, propKey, receiver);
          },
          set: function (target, propKey, value, receiver) {
            console.log(`setting ${propKey}!`);
            return Reflect.set(target, propKey, value, receiver);
          }
        });

        obj.count = 1
        //  setting count!
        ++obj.count
        //  getting count!
        //  setting count!
        //  2


"Proxy支持的拦截操作(13种), 可认为都是handler的属性"
    1.get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
    2.set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
    3.has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
    4.deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
    5.ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
    6.getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
    7.defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
    8.preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
    9.getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
    10.isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
    11.setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
    12.apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
    13.construct(target, args, newTarget):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
    
'总结'
1.只有apply和construct的target只能是函数对象,其他的都是对象
2.目前只有set和get可以进行继承,可以获取到receiver也就是"原始的读操作所在的那个对象"


'其他'
实例:Web 服务的客户端
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

const service = createWebService('http://example.com/data');
service.employees().then(json => {
  const employees = JSON.parse(json);
  // ···
});
上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl + '/' + propKey);
    }
  });
}
同理,Proxy 也可以用来实现数据库的 ORM 层。

get

1.get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为"目标对象"、"属性名"和"proxy实例本身"(严格地说,是原始的读操作所在的那个对象就是指向 Proxy 代理),其中最后一个参数可选。
 	"示例"
        var person = {
          name: "张三"
        };

        var proxy = new Proxy(person, {
          get: function(target, propKey) {
            if (propKey in target) {
              return target[propKey];
            } else {
              throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
            }
          }
        });

        proxy.name // "张三"
        proxy.age // 抛出一个错误

"get方法可以继承"
let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"
上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

'第三参数指向'
下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true
上面代码中,proxy对象的getReceiver属性会被get()拦截,得到的返回值就是proxy对象。


const proxy = new Proxy({}, {
  get: function(target, key, receiver) {
    return receiver;
  }
});
const d = Object.create(proxy);
d.a === d // true
上面代码中,d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。

"注意"
如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};
const proxy = new Proxy(target, handler);
proxy.foo // TypeError: Invariant check failed
数组读取负值的索引
function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c
实现函数链式操作
var pipe = function (value) {
  var funcStack = [];
  var oproxy = new Proxy({} , {
    get : function (pipeObject, fnName) {
      if (fnName === 'get') {
        return funcStack.reduce(function (val, fn) {
          return fn(val);
        },value);
      }
      funcStack.push(window[fnName]);
      return oproxy;
    }
  });

  return oproxy;
}

var double = n => n * 2;
var pow    = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63
生成各种 DOM 节点的通用函数dom
const dom = new Proxy({}, {
  get(target, property) {
    return function(attrs = {}, ...children) {
      const el = document.createElement(property);
      for (let prop of Object.keys(attrs)) {
        el.setAttribute(prop, attrs[prop]);
      }
      for (let child of children) {
        if (typeof child === 'string') {
          child = document.createTextNode(child);
        }
        el.appendChild(child);
      }
      return el;
    }
  }
});

const el = dom.div({},
  'Hello, my name is ',
  dom.a({href: '//example.com'}, 'Mark'),
  '. I like:',
  dom.ul({},
    dom.li({}, 'The web'),
    dom.li({}, 'Food'),
    dom.li({}, '…actually that\'s it')
  )
);

document.body.appendChild(el);

set

1.set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选

'set方法可以继承'
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    return true;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true

'第三参数指向'
跟get一样

"注意"
1.如果目标对象自身的某个属性不可写,那么set方法将不起作用。
const obj = {};
Object.defineProperty(obj, 'foo', {
  value: 'bar',
  writable: false
});

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = 'baz';
    return true;
  }
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"
上面代码中,obj.foo属性不可写,Proxy 对这个属性的set代理将不会生效。

2.set代理应当返回一个布尔值。严格模式下,set代理如果没有返回true,就会报错。
'use strict';
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 无论有没有下面这一行,都会报错
    return false;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'
上面代码中,严格模式下,set代理返回false或者undefined,都会报错。
数值不大于200
let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
内部属性不可读写
const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

apply

1.apply方法拦截函数的调用、call和apply操作。
2.apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

'示例'
var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p() // "I am the proxy"

has

1.has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
2.has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

"示例"
var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

"注意"
如果原对象不可配置或者禁止扩展,这时has()拦截会报错。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
  has: function(target, prop) {
    return false;
  }
});
'a' in p // TypeError is thrown

construct

1.construct()方法用于拦截new命令
2.construct可以接受三个参数,分别是"目标对象"、"构造函数的参数数组"、"创造实例对象时new命令作用的构造函数(可选)"

'示例'
const p = new Proxy(function () {}, {
  construct: function(target, args, newTarget) {
    console.log('called: ' + args.join(''));
    return { value: args[0] * 10 };
  }
});

(new p(1)).value
// "called: 1"
// 10

'注意'
1.construct()方法返回的必须是一个对象,否则会报错。
    const p = new Proxy(function() {}, {
      construct: function(target, argumentsList) {
        return 1;
      }
    });

    new p() // 报错

2.由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。
    const p = new Proxy({}, {
      construct: function(target, argumentsList) {
        return {};
      }
    });

    new p() // 报错
    // Uncaught TypeError: p is not a constructor

3.construct()方法中的this指向的是handler,而不是实例对象。
    const handler = {
      construct: function(target, args) {
        console.log(this === handler);
        return new target(...args);
      }
    }

    let p = new Proxy(function () {}, handler);
    new p() // true

deleteProperty

1.deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

'示例'
var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

'注意'
目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

defineProperty

1.defineProperty()方法拦截了Object.defineProperty()操作。

'示例'
var handler = {
  defineProperty (target, key, descriptor) {
    // 也可做defineProperty操作之后返回 true即可 
    return false; // 注意,这里的false只是用来提示操作失败,本身并不能阻止添加新属性。  
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效

'注意'
1.如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错。
2.如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty()方法不得改变这两个设置。

getOwnPropertyDescriptor

1.getOwnPropertyDescriptor()方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。

'示例'
var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }

getPrototypeOf

1.getPrototypeOf()方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
    Object.prototype.__proto__
    Object.prototype.isPrototypeOf()
    Object.getPrototypeOf()
    Reflect.getPrototypeOf()
    instanceof
        
'示例'
var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true

'注意'
1.getPrototypeOf()方法的返回值必须是对象或者null,否则报错。
2.如果目标对象不可扩展(non-extensible), getPrototypeOf()方法必须返回目标对象的原型对象。

isExtensible

1.isExtensible()方法拦截Object.isExtensible()操作。该方法只能返回布尔值,否则返回值会被自动转为布尔值

'示例'
var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log("called");
    return true;
  }
});

Object.isExtensible(p)
// "called"
// true

'注意'
这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。
Object.isExtensible(proxy) === Object.isExtensible(target)

下面是一个例子。
var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});

Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect exten

ownKeys

1.ownKeys()方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
    Object.getOwnPropertyNames()
    Object.getOwnPropertySymbols()
    Object.keys()
    for...in循环
2.ownKeys()方法返回的数组成员,只能是字符串或 Symbol 值

'示例'
let target = {
  a: 1,
  b: 2,
  c: 3
};
let handler = {
  ownKeys(target) {
    return ['a'];
  }
};
let proxy = new Proxy(target, handler);
Object.keys(proxy) // [ 'a' ]

'注意'
1.使用"Object.keys()"或者"for in"方法时,有三类属性会被ownKeys()方法自动过滤,不会返回。
    目标对象上不存在的属性
    属性名为 Symbol 值
    不可遍历(enumerable)的属性
    
    let target = {
      a: 1,
      b: 2,
      c: 3,
      [Symbol.for('secret')]: '4',
    };
    Object.defineProperty(target, 'key', {
      enumerable: false,
      configurable: true,
      writable: true,
      value: 'static'
    });
    let handler = {
      ownKeys(target) {
        return ['a', 'd', Symbol.for('secret'), 'key'];
      }
    };
    let proxy = new Proxy(target, handler);
    // console.log(Object.keys(proxy)) // ['a']
    for (let key in proxy) {
      console.log(key); // 只输出a
    }
    上面代码中,ownKeys()方法之中,显式返回不存在的属性(d)、Symbol 值(Symbol.for('secret'))、不可遍历的属性(key),结果都被自动过滤掉。
    
2.使用Object.getOwnPropertyNames()方法时会自动过滤属性名为 Symbol 值,其他的返回; 如果目标对象是不可扩展的(non-extensible),这时ownKeys()方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错

    let target = {
      a: 1,
      b: 2,
      c: 3,
      [Symbol.for('secret')]: '4',
    };
    Object.defineProperty(target, 'key', {
      enumerable: false,
      configurable: true,
      writable: true,
      value: 'static'
    });
    let handler = {
      ownKeys(target) {
        return ['a', 'd', Symbol.for('secret'), 'key'];
      }
    };
    let proxy = new Proxy(target, handler);
    console.log(Object.getOwnPropertyNames(proxy)) // ['a', 'd', 'key']

	————————————————————分隔符
    
    var obj = {
      a: 1
    };

    Object.preventExtensions(obj);

    var p = new Proxy(obj, {
      ownKeys: function(target) {
        return ['a', 'b'];
      }
    });

    Object.getOwnPropertyNames(p)
    // Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible

3.ownKeys()方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
    var obj = {};
    var p = new Proxy(obj, {
      ownKeys: function(target) {
        return [123, true, undefined, null, {}, []];
      }
    });
    Object.getOwnPropertyNames(p) // Uncaught TypeError: 123 is not a valid property name

preventExtensions

1.preventExtensions()方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true,否则会报错。
var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
上面代码中,proxy.preventExtensions()方法返回true,但这时Object.isExtensible(proxy)会返回true,因此报错。

为了防止出现这个问题,通常要在proxy.preventExtensions()方法里面,调用一次Object.preventExtensions()。
var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});
Object.preventExtensions(proxy)
// "called"
// Proxy {}

setPrototypeOf

1.setPrototypeOf()方法主要用来拦截Object.setPrototypeOf()方法。

'示例'
var handler = {
  setPrototypeOf (target, proto) {
    throw new Error('Changing the prototype is forbidden');
  }
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden

'注意'
该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型。

Proxy.revocable

Proxy.revocable()方法返回一个可取消的 Proxy 实例。
    let target = {};
    let handler = {};
    let {proxy, revoke} = Proxy.revocable(target, handler);
    proxy.foo = 123;
    proxy.foo // 123
    revoke();
    proxy.foo // TypeError: Revoked
Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

"Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。"

new Proxy和Proxy.revocable区别、this问题

'new Proxy和Proxy.revocable区别'  
  var person = {
    name: "张三"
  };

  var { ...res } = Proxy.revocable(person, { // res是{proxy, revoke}
    get: function (target, propKey) {
      if (propKey in target) {
        return target[propKey];
      } else {
        throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
      }
    }
  });


 var person = {
    name: "张三"
  };

  var { ...res } = new Proxy(person, { // res是{name}
    get: function (target, propKey) {
      if (propKey in target) {
        return target[propKey];
      } else {
        throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
      }
    }
  });


'this问题'
1.在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。
  var person = {
    name: "张三"
  };

  var proxy = new Proxy(person, {
    get: function (target, propKey) {

    }
  });
  console.log(proxy.name) // undefined

2.Proxy 拦截函数内部的this,指向的是handler对象。
    const handler = {
      get: function (target, key, receiver) {
        console.log(this === handler);
        return 'Hello, ' + key;
      },
      set: function (target, key, value) {
        console.log(this === handler);
        target[key] = value;
        return true;
      }
    };
    const proxy = new Proxy({}, handler);
    proxy.foo
    // true
    // Hello, foo
    proxy.foo = 1
    // true
    上面例子中,get()和set()拦截函数内部的this,指向的都是handler对象。

Reflect

1.Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
	'1'将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

	'2'修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }

    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }

	'3' 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
    // 老写法
    'assign' in Object // true
    // 新写法
    Reflect.has(Object, 'assign') // true

	'4'Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
    Proxy(target, {
      set: function(target, name, value, receiver) {
        var success = Reflect.set(target, name, value, receiver);
        if (success) {
          console.log('property ' + name + ' on ' + target + ' set to ' + value);
        }
        return success;
      }
    });
	
	'其他'有了Reflect对象以后,很多操作会更易读。
    // 老写法
    // .apply.call链式用法详见《apply、call、bind》目录
    Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
    // 新写法
    Reflect.apply(Math.floor, undefined, [1.75]) // 1


get

1.Reflect.get(target, propertyKey, receiver) 查找并返回target对象的name属性值,如果没有该属性,则返回undefined。如果目标值类型不是 Object,则抛出一个 TypeError。
    target 需要取值的目标对象
    propertyKey 需要获取的值的键值
    receiver 如果target对象中指定了getter,receiver则为getter调用时的this值。
'示例'
    var myObject = {
      foo: 1,
      bar: 2,
      get baz() {
        return this.foo + this.bar;
      },
    }

    Reflect.get(myObject, 'foo') // 1
    Reflect.get(myObject, 'bar') // 2
    Reflect.get(myObject, 'baz') // 3

'跟proxy类似其this指向会被改变,指向 Proxy 代理'
    var myObject = {
      foo: 1,
      bar: 2,
      get baz() {
        return this.foo + this.bar;
      },
    };

    var myReceiverObject = {
      foo: 4,
      bar: 4,
    };

    Reflect.get(myObject, 'baz', myReceiverObject) // 8

'注意'
    第一个参数不是对象,Reflect.get方法会报错。
    Reflect.get(1, 'foo') // 报错
    Reflect.get(false, 'foo') // 报错

set

1.Reflect.set(target, propertyKey, value, receiver) 设置target对象的name属性等于value。返回一个 Boolean 值表明是否成功设置属性。抛出一个 TypeError,如果目标不是 Object。
    target 设置属性的目标对象。
    propertyKey 设置的属性的名称。
    value 设置的值。
    receiver 如果遇到 setter,receiver则为setter调用时的this值。
'示例'
    var myObject = {
      foo: 1,
      set bar(value) {
        return this.foo = value;
      },
    }

    myObject.foo // 1
    Reflect.set(myObject, 'foo', 2);
    myObject.foo // 2
    Reflect.set(myObject, 'bar', 3)
    myObject.foo // 3

'跟proxy类似其this指向会被改变,指向 Proxy 代理'
    var myObject = {
      foo: 4,
      set bar(value) {
        return this.foo = value;
      },
    };

    var myReceiverObject = {
      foo: 0,
    };

    Reflect.set(myObject, 'bar', 1, myReceiverObject);
    myObject.foo // 4
    myReceiverObject.foo // 1

'注意'
1.如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。
    let p = {
      a: 'a'
    };
    let handler = {
      set(target, key, value, receiver) {
        console.log('set');
        Reflect.set(target, key, value, receiver)
      },
      defineProperty(target, key, attribute) {
        console.log('defineProperty');
        Reflect.defineProperty(target, key, attribute);
      }
    };

    let obj = new Proxy(p, handler);
    obj.a = 'A';
    // set
    // defineProperty

2.第一个参数不是对象,Reflect.set会报错。
    Reflect.set(1, 'foo', {}) // 报错
    Reflect.set(false, 'foo', {}) // 报错

apply

1.Reflect.apply(target, thisArg, args) 等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。返回值是调用完带着指定参数和 this 值的给定的函数后返回的结果。如果 target 对象不可调用,抛出 TypeError。
	target 目标函数。
	thisArgument target 函数调用时绑定的 this 对象。
	argumentsList target 函数调用时传入的实参列表,该参数应该是一个类数组的对象。

一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。
    const ages = [11, 33, 12, 54, 18, 96];
    // 旧写法
    const youngest = Math.min.apply(Math, ages);
    const oldest = Math.max.apply(Math, ages);
    const type = Object.prototype.toString.call(youngest);
    // 新写法
    const youngest = Reflect.apply(Math.min, Math, ages);
    const oldest = Reflect.apply(Math.max, Math, ages);
    const type = Reflect.apply(Object.prototype.toString, youngest, []);

has

1.Reflect.has(target, propertyKey) 对应name in obj里面的in运算符。一个 Boolean 类型的对象指示是否存在此属性。如果目标对象并非Object 类型,抛出TypeError。
	target 目标对象。
    propertyKey 属性名,需要检查目标对象是否存在此属性。

'示例'
    var myObject = {
      foo: 1,
    };
    // 旧写法
    'foo' in myObject // true
    // 新写法
    Reflect.has(myObject, 'foo') // true

construct

1.Reflect.construct(target, args) 等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。返回以target(如果newTarget存在,则为newTarget)函数为构造函数,argumentList为其初始化参数的对象实例。如果 target 或者 newTarget 不是构造函数,抛出TypeError,异常。
    target 被运行的目标构造函数
    argumentsList 类数组,目标构造函数调用时的参数。
    newTarget 可选 作为新创建对象的原型对象的 constructor 属性,参考 new.target 操作符,默认值为 target。

'示例'
    function Greeting(name) {
      this.name = name;
    }
    // new 的写法
    const instance = new Greeting('张三');
    // Reflect.construct 的写法
    const instance = Reflect.construct(Greeting, ['张三']);

deleteProperty

1.Reflect.deleteProperty(target, propertyKey) 等同于delete obj[name],用于删除对象的属性。Boolean 值表明该属性是否被成功删除,如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。。抛出一个 TypeError,如果target不是 Object。
	target 删除属性的目标对象。
	propertyKey 需要删除的属性的名称。

'示例'
    const myObj = { foo: 'bar' };
    // 旧写法
    delete myObj.foo;
    // 新写法
    Reflect.deleteProperty(myObj, 'foo');

defineProperty

1.Reflect.defineProperty(target, propertyKey, attributes) 基本等同于Object.defineProperty,用来为对象定义属性。'未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。'返回值为Boolean 值指示了属性是否被成功定义。如果target不是 Object,抛出一个 TypeError。
    target 目标对象。
    propertyKey 要定义或修改的属性的名称。
    attributes 要定义或修改的属性的描述。
    
'示例'
    function MyDate() {
      /*…*/
    }
    // 旧写法
    Object.defineProperty(MyDate, 'now', {
      value: () => Date.now()
    });
    // 新写法
    Reflect.defineProperty(MyDate, 'now', {
      value: () => Date.now()
    });

'其他'
这个方法可以与Proxy.defineProperty配合使用。
const p = new Proxy({}, {
  defineProperty(target, prop, descriptor) {
    console.log(descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
  }
});
p.foo = 'bar';
// {value: "bar", writable: true, enumerable: true, configurable: true}
p.foo // "bar"
上面代码中,Proxy.defineProperty对属性赋值设置了"拦截",然后使用Reflect.defineProperty完成了"赋值"。

getOwnPropertyDescriptor

1.Reflect.getOwnPropertyDescriptor(target, propertyKey) 等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,"将来会替代掉后者。"。如果属性存在于给定的目标对象中,则返回属性描述符;否则,返回 undefined。抛出一个 TypeError,如果目标不是 Object。
    target 需要寻找属性的目标对象。
    propertyKey 获取自己的属性描述符的属性的名称。
    
'示例'
    var myObject = {};
    Object.defineProperty(myObject, 'hidden', {
      value: true,
      enumerable: false,
    });
    // 旧写法
    var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
    // 新写法
    var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

'注意'
Reflect.getOwnPropertyDescriptor和Object.getOwnPropertyDescriptor的一个区别是,如果第一个参数不是对象,Object.getOwnPropertyDescriptor(1, 'foo')不报错,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')会抛出错误,表示参数非法。

getPrototypeOf

1.Reflect.getPrototypeOf(obj) 用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。给定对象的原型。如果给定对象没有继承的属性,则返回 null。如果 target 不是 Object,抛出一个 TypeError 异常。
	target 获取原型的目标对象。
    
'示例'
    const myObj = new FancyThing();
    // 旧写法
    Object.getPrototypeOf(myObj) === FancyThing.prototype;
    // 新写法
    Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

'注意'
Reflect.getPrototypeOf和Object.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。

isExtensible

1.Reflect.isExtensible (target) 对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。返回一个 Boolean 值表明该对象是否可扩展。抛出一个 TypeError,如果对象不是 Object。
	target 检查是否可扩展的目标对象。

'示例'
    const myObject = {};
    // 旧写法
    Object.isExtensible(myObject) // true
    // 新写法
    Reflect.isExtensible(myObject) // true

'注意'
如果参数不是对象,Object.isExtensible会返回false,因为非对象本来就是不可扩展的,而Reflect.isExtensible会报错。

ownKeys

1.Reflect.ownKeys (target) 方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。返回值是由目标对象的自身属性键组成的 Array。如果目标不是 Object,抛出一个 TypeError。
	target 获取自身属性键的目标对象。
    
'示例'
var myObject = {
  foo: 1,
  bar: 2,
  [Symbol.for('baz')]: 3,
  [Symbol.for('bing')]: 4,
};

// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]

// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]

preventExtensions

1.Reflect.preventExtensions(target) 对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。返回一个 Boolean 值表明目标对象是否成功被设置为不可扩展。抛出一个 TypeError 错误,如果 target 不是 Object。
	target 阻止扩展的目标对象。
    
'示例'
    var myObject = {};
    // 旧写法
    Object.preventExtensions(myObject) // Object {}
    // 新写法
    Reflect.preventExtensions(myObject) // true

'注意'
如果参数不是对象,Object.preventExtensions在 ES5 环境报错,在 ES6 环境返回传入的参数,而Reflect.preventExtensions都会报错。

setPrototypeOf

1.Reflect.setPrototypeOf(target, newProto)用于设置目标对象的原型(prototype)对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。返回一个 Boolean 值表明是否原型已经成功设置。如果 target 不是 Object ,或 *prototype *既不是对象也不是 null,抛出一个 TypeError 异常。
    target 设置原型的目标对象。
    prototype 对象的新原型(一个对象或 null)。
    
'示例'
    const myObj = {};
    // 旧写法
    Object.setPrototypeOf(myObj, Array.prototype);
    // 新写法
    Reflect.setPrototypeOf(myObj, Array.prototype);
    myObj.length // 0

'注意'
1.如果第一个参数不是对象,Object.setPrototypeOf会返回第一个参数本身,而Reflect.setPrototypeOf会报错。
2.如果第一个参数是undefined或null,Object.setPrototypeOf和Reflect.setPrototypeOf都会报错。
3如果无法设置目标对象的原型(比如,目标对象禁止扩展),Reflect.setPrototypeOf方法返回false。
    Reflect.setPrototypeOf({}, null)
    // true
    Reflect.setPrototypeOf(Object.freeze({}), null)
    // false

实例

'使用 Proxy 实现观察者模式'

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set}); // 进行拦截

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver); // 进行赋值
  // const result = target[key] = value; 也可以Proxy里自己进行赋值不适用Reflect.set()  
  queuedObservers.forEach(observer => observer());
  return result;
}
const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 李四, 20

apply、call、bind

1.call()、apply()和bind()方法 三者作用都是 改变this指向,这三个函数都是函数对象的方法,也就是说只有函数才可以直接调用这些方法

bind、call、apply都是用来指定一个函数内部的this的值, 先看看bind、call、apply的用法
    var year = 2021
    function getDate(month, day) {
      return this.year + '-' + month + '-' + day
    }

    let obj = {year: 2022}
    getDate.call(null, 3, 8)    //2021-3-8
    getDate.call(obj, 3, 8)     //2022-3-8
    getDate.apply(obj, [6, 8])  //2022-6-8
    getDate.bind(obj)(3, 8)     //2022-3-8

2.链式调用
    var console = window.console || {log: function () {}};   
    var log = console.log;  
    console.log = function(tips,message){   
    Function.prototype.apply.call(log, console, arguments);   
    Function.prototype.call.call(log, console, arguments);  
    Function.prototype.call.apply(log, [console, arguments]); 
	解析:
    	该怎么理解Function.prototype.apply.call(log,console,arguments);
            1.首先可以将Function.prototype.apply看成一个整体-->FunctionApply
            2.FunctionApply.call(log,console,arguments);
            3.那么将此句翻译一下 log.FunctionApply(console,arguments);
            4.然后再翻译一下,你就懂了吧,就是一个普通方法调用了console.log(arguments);
        
     '示例一'
        var f1=function(){console.log(1)};
        var f2=function(){console.log(2)};
        Function.prototype.call.call(Function.prototype.call,f2)//2
        Function.prototype.call.call(f1,f2);//1
        
     '示例二'
        var a = Function.prototype.call.apply( function(a){return a;},  [0,4,3] );
        alert(a);
        
'应用'
1.处理伪数组 (最常用)
    var arr = document.getElementsByTagName('li')获取了5个li元素,你现在需要获取其中的第2,3,4三个元素
    [].slice.call(arr,1,4);  // 推荐写法
        
2.继承

3.this 硬绑定 --- bind

知识借鉴:
    1.https://www.cnblogs.com/web-record/p/10477778.html
    2.https://blog.csdn.net/qq_43000315/article/details/125360096
    3.https://www.cnblogs.com/10yearsmanong/p/12208180.html

this

this指向:
	1. this 指向函数的调用对象
    2. this 指向事件的调用对象
    3. 在构造函数中,this指向实例对象
    4. 在prototype原型方法中的this,指向实例对象;
    5. 找不到函数的调用者,this指向window对象 (因为被window调用函数)

<div id="box">box</div>
  <script>
    // this是一个关键字,指向一个对象,在不同的调用环境this指向是不一样的

    // 1、全局this指window
    console.log(this)

    // 2、全局函数里的this指window
    function fn () {
      console.log(this)
    }
    fn()

    // 3、事件处理函数里的this指绑定事件的DOM对象(不一定是事件源,谁绑定就是谁)
    document.querySelector('#box').onclick = function () {
      console.log(this)
    }

    // 4、没有特别指向的匿名函数的this指window
    ;(function () {
      console.log(this)
    })()

    setTimeout(function () {
      console.log(this)
    }, 1000)

    // 包括数组的map,reduce等方法里面的匿名函数this都是指window
    // 匿名回调函数里的this也是window

    // 5、对象方法里的this指对象本身,不管是字面量声明还是原型方法或者class,this都是当前实例对象
    var obj = {
      name: 'lisi',
      say: function () {
        console.log(this) // obj
      }
    }

    // 6、构造函数里的this指将来new的实例
    function Dog (name) {
      this.name = name
    }

    var a = 2;
    function fun () {
        console.log(this.a);
    }
    var o = { a: 3, fun: fun }
    o.fun(); // 3  this指向o

    var p = { a: 4 };
    // 赋值语句的结果就是赋的值
    // 把赋值的结果也就是fun这个函数放进一个自调用函数里来执行,this指window
    (p.fun=o.fun)(); // 2
    
     //p.fun=o.fun; 
    //p.fun()// 4

    // 7、箭头函数没有自己的this
    var obj2 = {
      name: 'lisi',
      say: function () {
        // 点击box打印name
        document.querySelector('#box').onclick = () => {
          console.log(this.name)
        }
      }
    }
  </script>

修改this指向

<body>
  <script>
    var a = 10
    var obj1 = {
      a: 20,
      say: function (x, y) {
        console.log(this.a)
        console.log(this.a + x + y)
      }
    }
    var obj2 = {
      a: 30,
      say: function () {
        console.log(this.a)
          
        // bind是在函数封装的时候来修改this指向,这种的话以后的调用this都指obj1
      }.bind(obj1)
    }
    obj2.say()

    // 在调用的时候临时修改this
    obj1.say()
    // 在调用obj1.say的时候把this临时改为window
    obj1.say.call(window, 1, 2)

    // call和apply这两个方法作用是一样的,如果有参数传参不一样
    // call的参数直接挨个传递即可,apply后面的参数要放到一个数组里来传递
    obj1.say.apply(window, [1, 2])

    var obj3 = {
      a: 40,
      say: function () {
        setTimeout(function () {
          console.log(this.a)
            
          // bind就是把内层this绑定成外层this
        }.bind(this), 1000)
      }
    }
    obj3.say()

  </script>

函数节流和防抖

防抖:最常见的就是用户注册时候的手机号码验证和邮箱验证了。只有等用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。

节流:多数在监听页面元素滚动事件的时候会用到。因为滚动事件,是一个高频触发的事件。

函数防抖:如果一个功能要反复触发,但是多次触发会导致效果叠加,解决方案就是只在最后一次触发
    // 防抖的案例就是轮播图里的isMove,就是让中间的点击无效,只有最后一次静止了才有效
    // 函数防抖的处理手法一般就是标志位(开关)


    // 函数节流:如果一个功能要反复触发,但是多次触发会导致效果叠加
    //          解决方案是每次都触发,但是下一次的触发会先把上一次的效果清除
    // 开启一个新的定时器之前先把上一次的定时器清除 utils.move()


    // 瀑布流里的节流:
    // 我们自己的定时器会返回触发,但是每一次触发之前是把上一次的清除掉

    // 瀑布流里的防抖:
    // 另外用一个定时器来触发,如果滚动事件没有结束,第二次进来的时候这个定时器会被清除,里面的代码根本就不会运行
    // 只有最后一次滚动结束了,里面的自己的定时器代码才会开始运行

posted @ 2024-02-11 19:04  kocola  阅读(23)  评论(0编辑  收藏  举报