Bootstrap

深入理解JavaScript的美妙

javascript工作原理


JavaScript是单线程语言,这是因为js是浏览器脚本语言,处理用户交互和操作DOM,多线程会带来类似一个线程删除DOM节点一个在同一个DOM节点上添加内容的复杂问题,所以只有一个调用栈,因此,它按照语句出现的顺序执行的,同一时间只能做一件事。

  • v8引擎用于Chrome和node

    • 内存堆(Memory Heap):内存分配发生的地方
    • 调用栈(Call Stack):调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情
  • 很多引擎之外的 API,我们把这些称为浏览器提供的 Web API,比如说 DOM、AJAX、setTimeout等等

  • 事件循环Event Loop

    • 为解决页面卡顿,写了同步和异步任务
    • 同步任务进入主线程,异步任务进入Event Table(事件表)并注册函数
    • 当指定的事情完成后,Event Table将这个函数移入Event Queue(事件队列)
      • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
      • micro-task(微任务):Promise,process.nextTick
    • 主线程(Call Stack)执行完毕为空,会去将Event Queue里读取所有微任务,进入主线程执行,执行完所有微任务,读取一个宏任务执行,再执行所有微任务…
  • 执行上下文

    定义:执行上下文是当前js代码解析和执行代码时运行环境的抽象概念,任何代码执行都是在执行上下文中运行

    执行上下文分为 全局执行上下文函数执行上下文(eval用的少,不考虑)

    当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前调用栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
    引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

    • 全局执行上下文:一个程序只有一个,不在函数中的代码都位于全局执行上下文,它创建了全局对象window,this指向window

    • 函数执行上下文:在函数调用时都会创建一个函数执行上下文,也只能在函数调用时创建

    执行上下文生命周期(三个阶段):创建 --> 执行 --> 销毁

    js是单线程解释执行的脚步语言,在执行之前要先解析代码

    • 创建(解析)做的事情:
      • 确定this指向(当前的运行环境)

        • 只是直接调用 this --> window
        • obj.foo() this --> obj
        • var aa = new Foo() this --> aa
        • add.call(obj, 1,2) this --> obj
        • 箭头函数 this --> 外层的函数的this,若没有外层函数,指向window
        • setTimeout & setInterval 延时函数 this --> window
      • 词法环境

        • 环境记录器是存储变量和函数声明的实际位置。
        • 外部环境的引用意味着它可以访问其父级词法环境(作用域)
      • 变量环境

        • 也是词法环境,不同的是:词法环境被用来存储函数声明和变量(let 和 const)绑定,而变量环境只用来存储 var 变量绑定。

        注意:

          1. 在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)
          2. 这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
          3. 这就是我们说的变量声明提升。
          参考:https://juejin.im/post/5ba32171f265da0ab719a6d7#heading-0
        
      • 以下两点是es3,词法环境和变量环境的概念是es5

      • 创建变量对象–>初始化arguments, 然后提升函数声明和变量声明

      • 创建作用域链(作用域是变量的可访问性(可见性),由多个执行上下文的变量对象构成的链表叫作用域链)

  • 执行上下文栈:先进后出

  • 栈和堆:栈是一种后进先出的数据结构 那为什么引用值要放在堆中,而原始值要放在栈中. js中有一句话:能量是守衡的,无非是时间换空间,空间换时间的问题。 堆比栈大,栈比堆的运算速度快。

参考:

https://mp.weixin.qq.com/s/3fW_OvnyX-uk_9NfbJKcDw
https://segmentfault.com/a/1190000013915935
☆https://juejin.im/post/5ba32171f265da0ab719a6d7#heading-7

分割线

页面渲染过程

解析DOM,生成DOM树,加载css,生成css树,合并成render tree

  • CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但浏览器会"偷看"DOM,预先下载相关资源。
    • 因为js又可以改变DOM,白解析了
  • 浏览器遇到 <script>且没有defer或async属性的标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
    • 这就是js文件在css文件后面,要等css加载完毕,这样确保js获取最新的DOM
  • defer
    • defer属性延迟执行,不会延迟下载,浏览器遇到script就立即下载脚本,DOM同时解析
    • 文档解析完成时,脚本被执行,此时也会触发domcontentloaded事件,优先执行脚本
    • 多个标签添加defer属性,执行顺序仍然是按书写顺序
  • async
    • 让浏览器异步加载脚本文件。在加载脚本文件的时候,浏览器能继续标签的解析。
    • 加载完就执行,还是会发生阻塞,异步脚本一定会在load事件之前执行,但可能会在domcontentloaded事件之前或者之后执行。
    • 异步脚本之间的执行顺序不确定,可能不会按照书写顺序执行

同步加载以及defer、async加载时的区别,其中绿色线代表 HTML 解析,蓝色线代表网络读取js脚本,红色线代表js脚本执行时间
在这里插入图片描述
所以,你现在明白为何<script>最好放底部,<link>最好放头部,如果头部同时有<script><link>的情况下,最好将<script>放在<link>上面了吗?

分割线

增删改查

查找

  1. 已经站在一个节点上,找周围相邻的元素

    • 遍历节点父子关系:parentNode、parentElement、childNodes、children
    • 返回动态集合:firstChild、firstElementChild、lastChild、lastElementChild
    • 兄弟关系:previousSibling、previousElementSibling、nextSibling、nextElementSibling

    强调

     节点树: `缺:会受到空文本的干扰. 优:包含所有网页内容,没有兼容性问题`
     元素树:`缺:不受空文本的干扰. 优: 只包含元素节点,除children外,都有兼容性问题。`
    
  2. 在整个页面或在指定父元素下,根据html或选择器查找元素

    • 按HTML查找
    按Id:var elem=document.getElementById("id");
    按TagName:var elems=parent.getElementsByTagName("标签名");
    按Name:var elems=parent.getElementsByName("name");
    按ClassName:var elems=parent.getElementsByClassName("class")
    除ById外,都返回动态集合
    
  3. 不仅查找直接子元素,而且查找所有子代元素,如果经过复杂的选择逻辑,才能获得想要的元素时

    • 按选择器查找
    仅找一个元素:var elem=parent.querySelector("选择器");
    可能找到多个元素:var elems=parent.querySelectorAll("选择器")
    返回静态集合
    

    “静态集合” 和 "动态集合"的区别:

     静态集合是快照,不会因为之后的操作而变化
     动态集合会随着之后的操作变化
    
  4. 可直接获取的节点

    html:document.documentElement、head:document.head、body:document.body


修改

  • 操作节点内容:innerHTML没有兼容性问题

  • 操作节点属性

    • 核心DOM:getAttribute("属性名"),setAttribute("属性名","值")

    • HTML DOM:elem.属性名

    • HTML5:定义属性:data-属性名="值",访问:elem.dataset.属性名

  • 操作节点样式

    • 内联样式:elem.style.属性名,修改元素的样式都要用style
  • 计算后的样式

    • var style=window.getComputedStyle(elem,null/"::after")
      style.属性名

      区别:getComputedStyle返回元素的全部属性(预先定义,继承的,默认的),只读style。属性名返回预先定义的,可读写

    • 想要获得一个元素完整样式,就用getComputedStyle样式表中的样式

      var sheet=document.styleSheets[i]
      var rule=sheet.cssRules[i]
      rule.style.属性名
      

添加节点、添加元素

  1. 创建空元素对象:var a=document.createElement("a")

  2. 设置关键属性:a.href="http://imooc.con" a.innerHTML="go to imooc";

  3. 将新元素添加到指定父元素下

    追加:parent.appendChild(child)

    插入:parent.insertBefore(child,oldElem)

    替换节点:parent.replaceChild(child,oldElem)

优化

尽量少操作DOM树
1. 如果同时添加父元素和子元素,在内存中先将子元素添加到父元素,再将父元素整体挂到页面
2. 如果父元素已经在页面上,需要添加多个平级子元素时
(1)创建文档片段
    var frag=document.createDocumentFragment();
(2)将子元素添加到文档片段
    frag.appendChild(child);
(3)将文档片段添加到页面父元素下
    parent.appendChild(frag)

删除节点

parent.removeChild(child);
child.parentNode.removeChild(child)

分割线

数组及字符串常见的方法

https://mp.weixin.qq.com/s/L017-O3EVOjcV9tTHpYVIQ

不改变现有的数组:

+ concat:连接两个或多个数组
+ reduce(reduceRight从右开始):接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
+ every:检测数组所有元素是否都符合指定条件,返回true/false
+ some:检测数组中的元素是否满足指定条件,有一个元素满足条件,则表达式返回true
+ filter:创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
+ find(findIndex):返回传入一个测试条件(函数)符合条件的数组第一个元素(元素位置) / 没有符合的返回-1
+ forEach:用于调用数组的每个元素,并将元素传递给回调函数
+ map:一个新数组,数组中的元素为原始数组元素调用函数处理后的值
+ indexOf:返回某个指定的字符串值在字符串中首次出现的位置
+ lastIndexOf:返回一个指定的字符串值最后出现的位置
+ join:把数组中的所有元素转换一个字符串,元素是通过指定的分隔符进行分隔的
+ split:方法用于把一个字符串分割成字符串数组
+ slice:从已有的数组/字符串中返回选定的元素
+ string.substring(start, end):提取一个字符串,end不支持负数(负数为0)
+ string.substr(start, len)提取一个长度为len的字符串(start可以未负数)
+ sort:对数组的元素进行排序
+ copyWithin:从数组的指定位置拷贝元素到数组的另一个指定位置中,
  copyWithin(target, start, end)
+ isArray:Array.isArray直接写在了Array构造器上,而不是prototype对象上。Array.isArray会根据参数的[[Class]]内部属性是否是"Array" 返回true或false.

改变现有的数组:

+ push:向数组的末尾添加一个或多个元素,并返回新的长度
+ pop:删除最后一个元素,并返回删除的元素
+ unshift:向数组的起始位置添加一个或多个元素,并返回新的长度
+ shift:移除数组第一个元素
+ delete:移除对象或数组内某值,为empty
+ splice:用于插入、删除或替换数组的元素,
  [1,2,3].splice(开始删除/开始添加的index,要删除的个数,添加的数组)
+ fill:用于将一个固定值替换数组的元素
  var fruits = ["Banana", "Orange", "Apple"];
  fruits.fill("Runoob")=>fruits = ["Runoob","Runoob","Runoob"]
+ reverse()方法用于颠倒数组中元素的顺序
特殊例子:
var lis = document.getElementByTagName('li')
var lis2= [].slice.call(lis)
var lis2= Array.prototype.alice.call(lis)
lis非数组,lis2数组

map : [1,2,3].map(parseInt)
parseInt(1,0) 1
parseInt(2,1) NaN
parseInt(3,2) NaN

parseInt

分割线

JS方法

  1. Object.freeze(obj)

    可以冻结一个对象,防止对象被修改

  2. 判断数组方法的区别

    Array.isArray()、instantceof、Object.prototype.tostring.call

  • 两种整理
方法Array.isArrayinstanceofObject.prototype.toString.call
机制判断数组通过判断对象的原型链中是不是能找到类型的 prototype注1
检测数
据类型
数组对象对象(包括自定义实例化对象)和所有基本类型对象(不包括自定义实例化对象)和所有基本类型
优点当检测Array实例时,Array.isArray 优于 instanceof包括自定义实例化对象不包括自定义实例化对象
缺点只能判别数组是ES5新增,不存在可用Object.prototype.toString.call()instanceof 只能用来判断对象类型(引用类型),原始类型(基本类型)不可以,并且所有对象类型 instanceof Object 都是true不能精准判断自定义对象,对于自定义对象只会返回[object Object]
能否检测iframes(注2)不能

注1:每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文

注2 – 能否检测iframes:

var iframe = document.createElement(‘iframe’);
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr); // true
Object.prototype.toString.call(arr); // true
// Considered harmful, because doesn’t work though iframes
arr instanceof Array; // false

分割线

原型和原型链

终点为什么是null?

  1. 必须有一个停顿的位置,而不是无限的向上引用(循环引用)。
  2. 没法访问null的属性,起到了终止原型链的作用
  3. null在某种意义上也是一种对象,即空对象,不会违反“原型链上只能有对象”的约定

原型链,继承后有什么问题?详解JS原型链和继承:

简书:https://www.jianshu.com/p/dee9f8b14771
MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

参考:http://louiszhai.github.io/2015/12/15/prototypeChain/
https://blog.csdn.net/sysuzhyupeng/article/details/54846949
https://segmentfault.com/q/1010000002679144

概念:

构造函数,原型和实例的关系:
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针。

万物皆为对象,对象具有属性proto,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,这也保证了实例能够访问在构造函数原型中定义的属性和方法

JS对象的圈子里有这么个规则:如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.

如果让原型对象指向另一个类型的实例(constructor1.prototype = instance2)有趣的事情便发生了:

  • 查找属性

    • 首先会在instance1内部属性中找一遍;
    • 接着会在instance1.proto_(constructor1.prototype)中找一遍,而constructor1.prototype 实际上是instance2, 也就是说在instance2中寻找该属性p1;
    • 如果instance2中还是没有,此时程序不会灰心,它会继续在instance2._proto(constructor2.prototype)中寻找…直至Object的原型对象

    对应轨迹: instance1–> instance2 –> constructor2.prototype…–>Object.prototype

这种搜索的轨迹,形似一条长链, 又因prototype在这个游戏规则中充当链接的作用,于是我们把这种实例与原型的链条称作原型链.

    function Father(){
        this.property = true;
    }
    Father.prototype.getFatherValue = function(){
        return this.property;
    }
    function Son(){
        this.sonProperty = false;
    }
    //继承 Father
    Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
    Son.prototype.getSonVaule = function(){
        return this.sonProperty;
    }
    var instance = new Son();
    alert(instance.getFatherValue());//true
    instance实例通过原型链找到了Father原型中的getFatherValue方法

注意: 此时instance.constructor指向的是Father,这是因为Son.prototype中的constructor被重写的缘故.

如何判断原型和实例的这种继承关系?

  • 第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.
alert(instance instanceof Object);//true
alert(instance instanceof Father);//true
alert(instance instanceof Son);//true
由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 
因此, 这三个构造函数的结果都返回了true.
  • 第二种是使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示.
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
原理同上.

原型链的问题

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

为此,下面将有一些尝试以弥补原型链的不足.

借用构造函数

基本思想:即在子类型构造函数的内部调用超类型构造函数.
function Father(){
    this.colors = ["red","blue","green"];
}
function Son(){
    Father.call(this);// ☆☆继承了Father,且向父类型传递参数☆☆
}
var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"

var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的

很明显,借用构造函数一举解决了原型链的两大问题:

其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
其二, 子类型创建时也能够向父类型传递参数.

如果仅仅借用构造函数,将无法避免构造函数模式存在的问题

  • 方法都在构造函数中定义,因此函数复用也就不可用了.
  • 超类型(原型)中定义的方法,对子类型而言也是不可见的.
问题1理解:
this定义的方式,实例化之后是让每一个实例化对象都有一份属于自己的在构造函数中的对象或者函数方法,
prototype定义的方式,实例化之后每个实例化对象共同拥有一份构造函数中的对象或者函数方法。

组合继承

组合继承:指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.

基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.

既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性.

function Father(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
    alert(this.name);
};
function Son(name,age){
    Father.call(this,name);//继承实例属性,第一次调用Father()
    this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
    alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式. 而且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象.

问题:组合继承其实调用了两次父类构造函数, 造成了不必要的消耗,
后面讲到.

------以下是引子------

原型继承

在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

从本质上讲, object() 对传入其中的对象执行了一次浅复制. 下面我们来看看为什么是浅复制.

var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"

可以作为另一个对象基础的是person对象,于是我们把它传入到object()函数中,然后该函数返回一个新对象.这个新对象将person作为原型,因此它的原型中就包含引用类型值属性.这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享.

ES5通过新增 object.create() 方法规范化了上面的原型式继承

  • object.create() 接收两个参数:
    • 一个用作新对象原型的对象
    • (可选的)一个为新对象定义额外属性的对象
var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"

object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:

var person = {
    name : "Van"
};
var anotherPerson = Object.create(person, {
    name : {value : "Louis"}
});
alert(anotherPerson.name);//"Louis"

目前支持 Object.create() 的浏览器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.

提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路
寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象. 如下.

function createAnother(original){
    var clone = object(original);//通过调用object函数创建一个新对象
    clone.sayHi = function(){//以某种方式来增强这个对象
        alert("hi");
    };
    return clone;//返回这个对象
}

这个例子中的代码基于person返回了一个新对象–anotherPerson. 新对象不仅具有 person 的所有属性和方法, 而且还被增强了, 拥有了sayH()方法.

注意: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.

------引子结束------

寄生组合式继承

前面讲过,组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。寄生组合式继承就是为了降低调用父类构造函数的开销而出现的。

其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数

function extend(subClass,superClass){
    var prototype = object(superClass.prototype);//创建对象
    prototype.constructor = subClass;//增强对象
    subClass.prototype = prototype;//指定对象
}

extend的高效率

  • 它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要、多余的属性.
  • 原型链还能保持不变,因此还能正常使用 instanceof 和 isPrototypeOf() 方法。

以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法.

extend的另一种更为有效的扩展.

function extend(subClass, superClass) {
    // 完成了subClass和superClass的继承关系
    var F = function() {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F(); 
    // 明确构造器的指向
    subClass.prototype.constructor = subClass;
    // 跳出这个extend函数来看,之前传入的super完全也可以作为其他函数对象的sub!
    // 所以,在extend函数中,需要最后确认super的构造器指向是否正确,如果不正确,需要自己来更正。
    // 即使父类是Object也可以正常继承
    // superClass是从别的地儿继承过来(obj)又忘记指定constructor
    subClass.superclass = superClass.prototype;
    if(superClass.prototype.constructor == Object.prototype.constructor) {
    	superClass.prototype.constructor = superClass;
    }
}

我一直不太明白的是为什么要 “new F()“, 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作呢?
subClass.prototype = superClass.prototype;//直接指向超类型prototype

显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系.

new 运算符

为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情.

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);
第一行,我们创建了一个空对象obj;
第二行,我们将这个空对象的proto成员指向了F函数对象prototype成员对象;
第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.

我们可以这么理解: 以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:

1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this.

__proto__ 属性是指定原型的关键
以上, 通过设置 __proto__ 属性继承了父类, 如果去掉new 操作, 直接参考如下写法
subClass.prototype = superClass.prototype;//直接指向超类型prototype

那么, 使用 instanceof 方法判断对象是否是构造器的实例时, 将会出现紊乱.

假如参考如上写法, 那么extend代码应该为

function extend(subClass, superClass) {
  subClass.prototype = superClass.prototype;

  subClass.superclass = superClass.prototype;
  if(superClass.prototype.constructor == Object.prototype.constructor) {
    superClass.prototype.constructor = superClass;
  }
}

此时, 请看如下测试:

function a(){}
function b(){}
extend(b,a);
var c = new a(){};
console.log(c instanceof a);//true
console.log(c instanceof b);//true

c被认为是a的实例可以理解, 也是对的; 但c却被认为也是b的实例, 这就不对了. 究其原因, instanceof 操作符比较的应该是 c.proto 与 构造器.prototype(即 b.prototype 或 a.prototype) 这两者是否相等, 又extend(b,a); 则b.prototype === a.prototype, 故这才打印出上述不合理的输出.

那么最终,原型链继承可以这么实现,例如:

function Father(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
    alert(this.name);
};
function Son(name,age){
    Father.call(this,name);//继承实例属性,第一次调用Father()
    this.age = age;
}
extend(Son,Father)//继承父类方法,此处并不会第二次调用Father()
Son.prototype.sayAge = function(){
    alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10

分割线

引用类型

  • 基本类型:按值访问,可操作保存在变量中的实际的值。基本类型值指的是简单的数据段
    undefined、null、string、number、boolean、symbol
  • 引用类型:当复制保存着对象的某个变量时,操作的是对象的引用,但在为对象添加属性时,操作的是实际的对象。引用类型值指那些可能为多个值构成的对象。
    Object、Array、RegExp、Date、Function

区别:

  • 引用类型值可添加属性和方法,而基本类型值不可以
  • 在复制变量值时,基本类型会在变量对象上创建一个新值,再复制给新变量。两个变量的任何操作都不会影响到对方。而引用类型在创建一个对象类型时,在内存中开辟一个空间来存放值,我们要找到这个空间,需要知道这个空间的地址,变量存放的就是这个地址,复制变量时其实就是将地址复制了一份给新变量,两个变量的值都指向存储在堆中的一个对象,也就是说,其实他们引用了同一个对象,改变其中一个变量就会影响到另一个变量。
  • typeof:确定变量是字符串、数值、布尔值还是undefined的最佳工具。
  • instanceof :判断是否是某个对象类型。

分割线

实例化对象步骤

var a = new F()

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);
第一行,我们创建了一个空对象obj;
第二行,将空对象的__proto__成员指向了F函数对象prototype属性,该原型属性是一个原型对象,也就意味着obj的原型属性上拥有了F.prototype中的属性或方法
第三行,将F函数对象的this指针替换成obj,然后再调用F函数.obj有了F构造函数中的属性或方法,然后F函数无返回值或返回的不是对象,直接返回obj,否则返回F函数中的对象`

我们可以这么理解: 以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:

1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
2、属性和方法被加入到 this 引用的对象中。
3、新创建的对象由 this 所引用,并且最后隐式的返回 this.

分割线

作用域,作用域链

每个函数都有自己的执行环境,
当调用这个函数的时候(执行流进入一个函数)
函数的环境就会被推入一个环境栈中。
在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境
作用域:可访问的变量和函数的集合

当代码在一个环境中执行时会创建变量对象的作用域链,
作用域链:保证对执行环境中有权访问的所有变量和函数的有序访问
作用域链的前端始终是当前执行的代码所在的环境的所有变量对象,
如果这个环境是函数,则将其活动对象作为变量对象,
活动变量在最开始时只包含arguments,
作用域的下一个变量是外部环境,直到全局执行环境

分割线

闭包

闭包的概念
  • 闭包是指有权访问另一个函数作用域中的变量的函数,本质也就是在函数里面返回一个函数
  • 一般就是一个函数A,return其内部的函数B,被return出去的B函数能够在外部访问A函数内部的变量,这时候就形成了一个B函数的变量闭包,
  • A函数执行结束后这个变量背包也不会被销毁,并且这个变量背包在A函数外部只能通过B函数访问。
闭包形成的原理
  • 延长作用域链,当前作用域可以访问上级作用域中的变量
闭包解决的问题
  • 能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。
闭包带来的问题
  • 由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。
  • 所以使用完闭包后,可以通过对闭包 geta = null(下面代码) ,让垃圾回收器回收
实现一个简单的闭包
function fun(){
  var a = 10;//fun函数作用域内部的变量
  return ()=>{
    return a;//在此可以访问到fun函数作用域的a
  }
}

var geta = fun();
var a = geta();
console.log(a);//通过闭包我们就可以在fun函数外部访问到fun函数内部作用域的变量

JS闭包的9大经典使用场景:
https://cloud.tencent.com/developer/article/1842037?from=15425&areaSource=102001.2&traceId=wkM9axw16hHJjIP4ihvhj

闭包的作用
  • 延迟变量的生命周期
  • 创建出私有作用域 (如 vue中data返回一个对象)
  • 闭包可以在函数外部访问到函数内部作用域的变量
  • 闭包可以让访问变量不会被垃圾机制回收
  • 因为受js链式作用域的影响,
  • 子对象会一级一级向上寻找所有父对象的变量,反之不行。
  • js中函数内部可以读取全局变量,函数外部不能读取函数内部的局部变量

分割线

防抖和节流

都是防止某事件频繁发生
  1. 防抖
    思路:高频操作触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
    思路:每次触发事件时都取消之前的延时调用方法
    场景:输入联想、resize
function debounce(fn) {
	  // 创建一个标记用来存放定时器的返回值
      let timeout = null; 
      return function () {
      	// 每当用户输入的时候把前一个 setTimeout clear 掉
        clearTimeout(timeout); 
        timeout = setTimeout(() => { 
          // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后
          // 的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
          fn.apply(this, arguments);
        }, 500);
      };
    }
    function sayHi() {
      console.log('防抖成功');
    }
    var inp = document.getElementById('inp');
    inp.addEventListener('input', debounce(sayHi)); // 防抖
  1. 节流
    思路:n秒内置只执行一次某高频操作,所以节流是稀释函数的执行频率
    思路:触发事件时,判断是否有等待执行的延时函数
    场景:mousedown,滚动事件
function throttle(fn) {
      let canRun = true; // 通过闭包保存一个标记
      return function () {
        if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
        canRun = false; // 立即设置为false
        setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
          fn.apply(this, arguments);
          // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
          canRun = true;
        }, 500);
      };
    }
    function sayHi(e) {
      console.log(e.target.innerWidth, e.target.innerHeight);
    }
    window.addEventListener('resize', throttle(sayHi));

分割线

跨域

  • 什么是跨域?

      一个域下的文档或脚本试图去请求另一个域下的资源
    
  • 为什么会有跨域限制?

    浏览器的基本安全功能 – 同源策略,规定协议、端口、域名(IP)只要有一个不同即是跨域

  • 限制哪些行为?

    1. Cookie、LocalStorage 和 IndexDB 无法读取
    2. DOM 和 Js对象无法获得
    3. AJAX 请求不能发送
  • 不限制哪些行为?

    1. 资源跳转: A链接、重定向、表单提交
    2. 资源嵌入: <link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
    3. 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

如何解决跨域

1、 通过jsonp跨域
    原理:利用script允许跨域的原理
    
2、 document.domain + iframe跨域
    仅限主域相同,子域不同的跨域应用场景
    原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
    父窗口:http://www.domain.com/a.html
    子窗口:http://child.domain.com/a.html
    
3、 location.hash + iframe
    原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
    
4、 window.name + iframe跨域
    原理:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
        通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
    
5、 postMessage跨域
    postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,
    它可用于解决以下方面的问题:
    a.) 页面和其打开的新窗口的数据传递
    b.) 多窗口之间消息传递
    c.) 页面与嵌套的iframe消息传递
    
6、 跨域资源共享(CORS)
    服务端:Access-Control-Allow-Origin
    客户端:withCredentials(如果要带cookie请求)
    
7、 nginx代理跨域
    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
    实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

8、 nodejs中间件代理跨域
    大致与nginx相同
    非vue利用中间件服务器
    vue利用webpack的devServer(webpack-dev-server)
    
9、 WebSocket协议跨域
    WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

参考:
https://segmentfault.com/a/1190000011145364#articleHeader1

分割线

深拷贝

为什么要深拷贝:

js原始数据类型:Undefined, Null, String, Number, Boolean, Symbol
引用类型:Object、Array、RegExp、Date、Function
引用类型被clone的是堆地址,也就说原对象值变了,克隆后的值也跟着变了

简单深拷贝:

var obj = {a:[1,2],b:{aa:11}}
var obj_copy = JSON.parse(JSON. stringify())

弊端:

--- 不能够拷贝function,symbol,regExp等特殊对象的克隆
--- 对象又循环引用会报错
--- 抛弃了对象的constructor, 所有的构造函数指向Object

深拷贝思路:

1. 深拷贝=浅拷贝+递归
2. 参数校验	-- Object.prototype.toString.call(x)==='[object Object]'
3. 是否是对象判断要严谨、数组兼容
4. 递归爆栈问题:递归 改为 深度优先
5. 循环引用问题:引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

参考:

https://segmentfault.com/a/1190000016672263
https://juejin.im/post/5abb55ee6fb9a028e33b7e0a

分割线

事件捕获

事件传播有三个阶段:

1. 捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素。
2. 目标阶段–事件已达到目标元素。
3. 冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。

id.outter.addEventListener('click',function(){
    alert('id')
}, false)

第三个参数 true/false :事件在捕获/冒泡阶段发生,默认false
event.stopPropagation() :阻止冒泡或捕获

event.currentTarget、event.target区别?

  • event.currentTarget是绑定事件的元素
  • event.target触发事件的元素

分割线

== 和 === 比较

基本类型比较:

== 转换成相同类型,比较值是否相等,=== 类型不同就是不等

引用类型比较:

== 和 === 没有区别

基本类型和引用类型比较:

== 引用类型转换为基本类型,比较值,=== 为false

nullundefined true
null
=undefined false
'2’2 true
false
0 true
false1 true
[1,2]
‘1,2’ true

分割线

浮点数精度

– https://juejin.im/post/5dd7aef0f265da7de15d19c3

1. 出现原因:由于计算机的二进制实现和位数限制
    eg: (符号位) Mantissa x Base^Exponent(尾数x 基数^指数)
(1) 1位用来表示符号位
(2) 11位用来表示指数
(3) 52位表示尾数
** 浮点数 **
0.1 = 0.0001 1001 1001 ...
0.1 * 2 = 0.2 ---- 0
0.2 * 2 = 0.4 ---- 0
0.4 * 2 = 0.8 ---- 0
0.8 * 2 = 1.6 ---- 1
0.6 * 2 = 1.2 ---- 1
0.2 * 2 = 0.4 ---- 0
0.4 * 2 = 0.8 ---- 0
0.8 * 2 = 1.6 ---- 1
0.6 * 2 = 1.2 ---- 1
...
当进行计算或其他操作时时,四舍五入(逢1进,逢0舍)

** 大整数 **
大整数也存在同样的问题,因为表示尾数的尾数只有52位,
因此JS 中能精准表示的最大整数是 
Math.pow(2, 53),即十进制9007199254740992。

2. 常见错误
0.1+0.2!=0.3

9999999999999999 == 10000000000000001 true

parseFloat(0.9);    //0.9
parseFloat(9999999999999999.9)    //10000000000000000
parseInt("9999999999999999");    //10000000000000000
parseFloat(9.999999999999999);   //10

var num = 1.335;
num.toFixed(2);   //1.33 (没有进行四舍五入)

3. 解决方案
(1)转换为整数
(2)后台转换
(3)重写toFixed

function toInt(num){
    var rel = {}
    var str = num<0?-num+'':num+''
    var pos = num.indexOf('.')
    var len = num.substr(pos+1).length
    var times = Math.pow(10, len)
    rel.times = times
    rel.num = num
    return rel
}
eg. 0.1+0.25
var d1 = toInt(0.1)
var d2 = toInt(0.23)
var max = d1.times>d2.times?d1.times:d2.times
0.1+0.25 => (d1.num*max+d2.num*max)/max
0.1-0.25 => (d1.num * max - d2.num * max) / max;
0.1*0.25 => ((d1.num * max) * (d2.num * max)) / (max * max);
0.1/0.25 => (d1.num * max) / (d2.num * max);

function toFixed(num, s){
    var times = Math.pow(10, s)
    var des = num*times+0.5
    var des = parseInt(des, 10)/times
    return des + ''
}

分割线

AST抽象语法树

参考:https://juejin.im/entry/5b8ba64051882543036711dd




JSBridge的原理

https://juejin.im/post/5abca877f265da238155b6bc

分割线

正则

分割线

es6

参考:http://es6.ruanyifeng.com/

浏览器兼容es6的情况:https://kangax.github.io/compat-table/es6/

  • ECMAScript 和 JavaScript的关系是:前者是后者的规范、标准,后者是前者的一种实现
  • ES6是一种泛指,含义是5.1以后的JavaScript的下一代标准,涵盖了 ES2015、ES2016、ES2017,ES2015 则是正式名称,所以一般ES6指的是ES2015或下一代JavaScript语言标准
  • ECMAScript 的历史:
    • ECMAScript 1.0 – 1997
    • ECMAScript 2.0 – 1998 年 6 月
    • ECMAScript 3.0 – 1999 年 12 月(巨大成功)
    • ECMAScript 4.0 – 2000年开始酝酿
    • ECMAScript 3.1 – 因ECMAScript 4.0对ES3做了彻底升级,标准委员会分歧太大,将其中涉及现有功能改善的一小部分发布为ECMAScript 3.1
    • ECMAScript 5-- 2009 年 12 月,将ECMAScript 3.1改名为ECMAScript 5(Harmony 项目),一些较为可行的定位 JavaScript.next 继续开发,后来演变成ECMAScript 6
    • ECMAScript 5.1 – 2011 年 6 月
    • ECMAScript 6 – 2013 年 3 月,ECMAScript 6草案冻结,不再添加新功能
    • ECMAScript 6 草案发布 – 2013 年 12 月,12 个月的讨论期
    • ECMAScript 6正式通过 – 2015 年 6 月,成为国际标准,从 2000 年算起,这时已经过去了 15 年。
    • nodejs是JavaScript的服务器运行环境,对es6的支持度很高
  • Babel是es6广泛使用的转码器,能够把es6转为es5,这样就可以用es6的方式编写程序而不担心浏览器不支持
    • npm install --save-dev @babel/core
    • 配置文件.babelrc
      • 该文件用来设置转码规则和插件
        {
            // 最新转码规则:npm install --save-dev @babel/preset-env
            // react转码规则: npm install --save-dev @babel/preset-react
            "presets": [
                "@babel/env",
                "@babel/preset-react"
            ],
            "plugins": []
        }
        
  • Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
    eg:ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill
var、let、const
  • var:没有块作用域概念,变量提升
  • let:块作用域,不会变量提升,声明变量之前,该变量都是不可用的,同一块内不能重复声明赋值,可以代替立即执行匿名函数
  • const:用来常量,一旦声明常量,不能改变(不起作用,不报错,默默的失败),不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
    const与let一样只在块作用域有效,不能重复声明赋值
解构赋值

解构:从数组/对象中提取值,对变量进行赋值

数组:

1. 只要两边模式相同,左边的变量就会被赋予对应的值
let [a, b, c] = [1, 2, 3] => a=1,b=2,c=3
let [foo,[[bar],baz]]=[1, [[2], 3]] => foo=1, bar=2, baz=3
let [,,third] = ["foo", "bar", "baz"] => third = "baz"
let [head, ...tail] = [1, 2, 3, 4] => head=1, tail=[2, 3, 4]
let [x, y, ...z] = ['a'] => x="a",y=undefind,z=[]

2. 如果解构不成功,变量的值就等于undefined。
let [foo] = [];
let [bar, foo] = [1];
=> foo = undefined

3. 等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
=> 报错

4. 解构赋值允许指定默认值(只有当一个数组成员严格等于(===)undefined,默认值才会生效)
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x = 1] = [null]; // x = null

5. 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
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 } = { foo: 'aaa', bar: 'bbb' } 
    // foo="aaa", bar="bbb"

2. 如果解构失败,变量的值等于undefined。
    let {foo} = {bar: 'baz'} => foo=undefined

3. 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量
    // 将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上
    let { log, sin, cos } = Math;
    const { log } = console => log('hello') // hello

4. 如果变量名与属性名不一致,如下写法:
    let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
    baz // "aaa"
    所以:
    let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' }
    简写:let { bar, foo } = { foo: 'aaa', bar: 'bbb' } 
    
    对象的解构赋值的内部机制:
    先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者

5. 嵌套结构的对象
    let obj = {
      p: ['Hello', { y: 'World' }]
    };
    let { p, p: [x, { y }] } = obj;
    x // "Hello"
    y // "World"
    p // ["Hello", {y: "World"}]
    
6. 解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错
    // 报错
    let {foo: {bar}} = {baz: 'baz'};
    原因:foo这时等于undefined,再取子属性就会报错
    
7. 对象的解构赋值可以取到继承的属性
    const obj1 = {};
    const obj2 = { foo: 'bar' };
    // setPrototypeOf:设置对象obj1的原型为obj2
    Object.setPrototypeOf(obj1, obj2);
    
    const { foo } = obj1;
    foo // "bar"
    
8. 可以设置默认值
    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
    
    默认值生效的条件是,对象的属性值严格等于undefined
    var {x = 3} = {x: undefined} => x = 3
    var {x = 3} = {x: null} => x = null
    
9. 注意点:
    (1)JavaScript 引擎会将{x}理解成一个代码块
        // 错误的写法
        let x; 
        {x} = {x: 1};
        
        // 正确的写法
        let x;
        ({x} = {x: 1});
    (2)解构赋值允许等号左边的模式之中,不放置任何变量名
        // 古怪但正确
        ({} = [true, false]);
        ({} = 'abc');
        ({} = []);
    (3)数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
        let arr = [1, 2, 3];
        let {0 : first, [arr.length - 1] : last} = arr;
        first // 1
        last // 3
        
        
字符串:

    字符串被转换成了一个类似数组的对象
    const [a, b, c, d, e] = 'hello';
    a // "h"
    b // "e"
    c // "l"
    d // "l"
    e // "o"
    
    类似数组的对象都有一个length属性
    let {length : len} = 'hello';
    len // 5


数值和布尔值:

    等号右边是数值和布尔值,则会先转为对象
    let {toString: s} = 123;
    s === Number.prototype.toString // true
    
    let {toString: s} = true;
    s === Boolean.prototype.toString // true
    数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
    
    解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。
    由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
    let { prop: x } = undefined; // TypeError
    let { prop: y } = null; // TypeError
    
    
函数参数:

    1. // 表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y
    function add([x, y]){
      return x + y;
    }
    add([1, 2]); // 3
    
    [[1, 2], [3, 4]].map(([a, b]) => a + b);
    // [ 3, 7 ]
    
    2. 指定默认值不同
    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]
    
    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]
    
    3. undefined就会触发函数参数的默认值。
    [1, undefined, 3].map((x = 'yes') => x);
    // [ 1, 'yes', 3 ]
    
    
解构赋值的用途
    
    1.交换变量值
        let x = 1;
        let y = 2;
        [x, y] = [y, x];
        
    2.从函数返回多个值,不用放到数组或对象里再取,取值方便
        // 返回一个数组
        function example() {
          return [1, 2, 3];
        }
        let [a, b, c] = example();
        
        // 返回一个对象
        function example() {
          return {
            foo: 1,
            bar: 2
          };
        }
        let { foo, bar } = example();
        
    3.函数参数的定义,将参数和变量名对应起来
        // 参数是一组有次序的值
        function f([x, y, z]) { ... }
        f([1, 2, 3]);
        // 参数是一组无次序的值
        function f({x, y, z}) { ... }
        f({z: 3, y: 2, x: 1});
        
    4. 提取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]
        
    5.函数参数的默认值
        jQuery.ajax = function (url, {
          async = true,
          beforeSend = function () {},
          cache = true,
          complete = function () {},
          crossDomain = false,
          global = true,
          // ... more config
        } = {}) {
          // ... do stuff
        };
        避免了var foo = config.foo || 'default foo';这样的语句
    
    6.遍历map结构
        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) {
          // ...
        }
        
    7.输入模块的指定方法
        const { SourceMapConsumer, SourceNode } = require("source-map");
字符串的扩展
模板字符串 ``
    1.普通字符串
        `In JavaScript '\n' is a line-feed.`
    
    2.多行字符串
        `In JavaScript this is
         not legal.`
    
    3.字符串中嵌入变量,大括号内部可以放入任意的 JavaScript 表达式
        let name = "Bob", time = "today";
        `Hello ${name}, how are you ${time}?`
    
    4.模板中需要用到反引号,前面要用反斜杠转义
        let greeting = `\`Yo\` World!`;
        
    5.所有的空格和缩进都会被保留在输出之中
        $('#list').html(`
        <ul>
          <li>first</li>
          <li>second</li>
        </ul>
        `);
        
    6.可以嵌套
        const tmpl = addrs => `
          <table>
          ${addrs.map(addr => `
            <tr><td>${addr.first}</td></tr>
            <tr><td>${addr.last}</td></tr>
          `).join('')}
          </table>
        `;
        
新增方法:
    
1.includes:返回布尔值,表示是否找到了参数字符串
2.startsWith:返回布尔值,表示参数字符串是否在原字符串的头部
3.endsWith:返回布尔值,表示参数字符串是否在原字符串的尾部
4.repeat:返回新字符串,将原字符串重复n次
    'hello'.repeat(2) // "hellohello"
5.字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全
    padStart()用于头部补全
        'x'.padStart(5, 'ab') // 'ababx'
    padEnd()用于尾部补全
        'x'.padEnd(4, 'ab') // 'xaba'
    用途:
        补全位数:'1'.padStart(10, '0') // "0000000001"
        '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
        '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
6.trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格
    const s = '  abc  ';
    s.trim() // "abc"
    s.trimStart() // "abc  "
    s.trimEnd() // "  abc"
7.matchAll:正则表达式在当前字符串的所有匹配
数组的扩展
扩展运算符 (...) : 扩展运算符(...)内部使用for...of循环
    
1.将一个数组转为用逗号分隔的参数序列
    console.log(...[1, 2, 3])
    // 1 2 3
    console.log(1, ...[2, 3, 4], 5)
    // 1 2 3 4 5
    [...document.querySelectorAll('div')]
    // [<div>, <div>, <div>]
    
2. 替代函数的 apply 方法
    // ES5 的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f.apply(null, args);
    
    // ES6的写法
    function f(x, y, z) {
      // ...
    }
    let args = [0, 1, 2];
    f(...args);
    
    // ES5 的写法
    Math.max.apply(null, [14, 3, 77])
    // ES6 的写法
    Math.max(...[14, 3, 77])
    // 等同于
    Math.max(14, 3, 77);
    
    // ES5的 写法
    var arr1 = [0, 1, 2];
    var arr2 = [3, 4, 5];
    Array.prototype.push.apply(arr1, arr2);
    // ES6 的写法
    let arr1 = [0, 1, 2];
    let arr2 = [3, 4, 5];
    arr1.push(...arr2);

3. 扩展运算符的应用
    复制数组
        ES5复制数组
            const a1 = [1, 2];
            const a2 = a1.concat();
            a2[0] = 2;
            a1 // [1, 2]
        扩展运算符
            const a1 = [1, 2];
            // 写法一
            const a2 = [...a1];
            // 写法二
            const [...a2] = a1;
            
    数组合并(浅拷贝)
        // ES5 的合并数组
        arr1.concat(arr2, arr3);
        // [ 'a', 'b', 'c', 'd', 'e' ]
        // ES6 的合并数组
        [...arr1, ...arr2, ...arr3]
        // [ 'a', 'b', 'c', 'd', 'e' ]
        
    与解构赋值结合
        const [first, ...rest] = [1, 2, 3, 4, 5];
        first // 1
        rest  // [2, 3, 4, 5]
        
        const [first, ...rest] = [];
        first // undefined
        rest  // []
        
        const [first, ...rest] = ["foo"];
        first  // "foo"
        rest   // []
        
        扩展运算符,只能放在参数的最后一位,否则会报错
        const [...butLast, last] = [1, 2, 3, 4, 5];
        // 报错
        const [first, ...middle, last] = [1, 2, 3, 4, 5];
        // 报错
        
    字符串
        将字符串转为真正的数组
        [...'hello']
        // [ "h", "e", "l", "l", "o" ]
        能够正确识别四个字节的 Unicode 字符
        'x\uD83D\uDE80y'.length // 4
        [...'x\uD83D\u`
        
    实现了 Iterator 接口的对象
        任何定义了遍历器(Iterator)接口的对象(参阅Iterator 一章),都可以用扩展运算符转为真正的数组
        
        
Array.from()

    1. 用于将两类对象转为真正的数组:
        类似数组的对象
            有length属性的对象
            
        可遍历(有Symbol.iterator接口)的对象
            Array
            Map
            Set
            String
            TypedArray
            函数的 arguments 对象
            NodeList 对象

        let arrayLike = {
            '0': 'a',
            '1': 'b',`
            '2': 'c',
            length: 3
        };
        // ES5的写法
        var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
        // ES6的写法
        let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
    
    2. 扩展运算符(...)也可以将任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
    和Array.from的区别:
        Array.from可以转换类似数组的对象(有length属性的对象)为数组
        
        Array.from({ length: 3 });
        // [ undefined, undefined, undefined ]
    
    3.Array.from的第二个参数(类似map)
        let spans = document.querySelectorAll('span.name');
        // map()
        let names1 = Array.prototype.map.call(spans, s => s.textContent);
        // Array.from()
        let names2 = Array.from(spans, s => s.textContent)
        
        Array.from([1, , 2, , 3], (n) => n || 0)
        // [1, 0, 2, 0, 3]
        
        function typesOf () {
          return Array.from(arguments, value => typeof value)
        }
        typesOf(null, [], NaN)
        // ['object', 'object', 'number']
        
        Array.from({ length: 2 }, () => 'jack')
        // ['jack', 'jack']
        
        // 避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。
        Array.from(string).length;
        
        如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。
        
Array.of() 将一组值,转换为数组

    1.弥补数组构造函数Array()的不足
        Array() // []
        Array(3) // [, , ,]
        Array(3, 11, 8) // [3, 11, 8]
        不少于 2 个时,Array()才会返回由参数组成的新数组。
        参数个数只有一个时,实际上是指定数组的长度
        
        Array.of() // []
        Array.of(undefined) // [undefined]
        Array.of(1) // [1]
        Array.of(1, 2) // [1, 2]
        行为非常统一
    
    2.模拟实现
        function ArrayOf(){
            return [].slice.call(arguments);
        }
    
新增方法
    copyWithin()
        将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。使用这个方法,会修改当前数组。
        
        Array.prototype.copyWithin(target, start = 0, end = this.length)
        target(必需):从该位置开始替换数据。如果为负值,表示倒数。
        start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
        end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
        
        // 将3号位复制到0号位
        [1, 2, 3, 4, 5].copyWithin(0, 3, 4)
        // [4, 2, 3, 4, 5]
    
    find:找出第一个符合条件的数组成员
    findIndex:返回第一个符合条件的数组成员的位置
    
    fill():
        使用给定值,填充一个数组
            ['a', 'b', 'c'].fill(7)
            // [7, 7, 7]
        fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
            ['a', 'b', 'c'].fill(7, 1, 2)
            // ['a', 7, 'c']
        注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
            let arr = new Array(3).fill({name: "Mike"});
            arr[0].name = "Ben";
            arr
            // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
            
            let arr = new Array(3).fill([]);
            arr[0].push(5);
            arr
            // [[5], [5], [5]]
            
    flat():将嵌套的数组“拉平”,变成一维的数组
        [1, 2, [3, 4]].flat()
        // [1, 2, 3, 4]
        
        默认只会“拉平”一层,参数为想要拉平的层数
        [1, 2, [3, [4, 5]]].flat(2)
        // [1, 2, 3, 4, 5]
        如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数
        [1, [2, [3]]].flat(Infinity)
        // [1, 2, 3]
        
    flatMap():对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法
        // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
        [2, 3, 4].flatMap((x) => [x, x * 2])
        // [2, 4, 3, 6, 4, 8]
Set、Map、WeakSet、WeakMap
Set

1. Set类似于数组,但Set结构不会添加重复的值
    var s = new Set()
    [2,3,4,5,6,2,2].forEach(x => s.add(x))
    for (let i of s) {
        console.log(i);
    }
    = 2,3,4,5,6
    
2.Set函数接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化
    // 例一
    const set = new Set([1, 2, 3, 4, 4]);
    [...set]
    // [1, 2, 3, 4]
    set.size // 5
    
    // 例三
    const set = new Set(document.querySelectorAll('div'));
    set.size // 56

3. 去重
    去除数组的重复成员
    [...new Set(array)]
    除字符串里面的重复字符。
    [...new Set('ababbc')].join('')
    // "abc"
    
4. Set判断两个值是否相同,用Same-value-zero equality(类似===),和===的区别是:NaN===NaN => false,后者为true,如下:
    let set = new Set();
    let a = NaN;
    let b = NaN;
    set.add(a);
    set.add(b);
    set // Set {NaN}
    
    另外,两个对象总是不相等的
        let set = new Set();
        set.add({});
        set.size // 1
        set.add({});
        set.size // 2

5. Set 实例的属性和方法
    Set.prototype.constructor:构造函数,默认就是Set函数。
    Set.prototype.size:返回Set实例的成员总数。
    Set.prototype.add(value):添加某个值,返回 Set 结构本身。
    Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
    Set.prototype.clear():清除所有成员,没有返回值。
    
    Array.from方法可以将 Set 结构转为数组(顺带去重)
    const items = new Set([1, 2, 3, 4, 5]);
    const array = Array.from(items);

6. 遍历操作
    Set.prototype.keys():返回键名的遍历器
    Set.prototype.values():返回键值的遍历器
    Set.prototype.entries():返回键值对的遍历器
    Set.prototype.forEach():使用回调函数遍历每个成员

Map 类似对象,但“键”的范围不限于字符串
Iterator
1. 表示“集合”的数据结构:Array、Object、Set、Map
2. 遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作
3. Iterator 的作用:
    + 为各种数据结构,提供一个统一的、简便的访问接口
    + 使得数据结构的成员能够按某种次序排列
    + ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费
4.Iterator 的遍历过程:
    (1)创建一个指针对象,指向当前数据结构的起始位置
    (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
    (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
    (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
        模拟:
        var it = makeIterator(['a', 'b']);
        it.next() // { value: "a", done: false }
        it.next() // { value: "b", done: false }
        it.next() // { value: undefined, done: true }
        
        function makeIterator(array) {
          var nextIndex = 0;
          return {
            next: function() {
              return nextIndex < array.length ?
                {value: array[nextIndex++], done: false} :
                {value: undefined, done: true};
            }
          };
        }
    5.默认 Iterator 接口
        (1).当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
        (2).数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)
        (3).ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”
    5.1.原生具备 Iterator 接口的数据结构如下。
        Array
        Map
        Set
        String
        TypedArray
        函数的 arguments 对象
        NodeList 对象

for...of与其他遍历语法的比较
    for:写法麻烦
    forEach:写法简便,但不能用break、continue跳出
    for...in:遍历数组的键名,适合遍历对象
        缺点:
        + 键名是字符串,如'0','1'
        + for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
        + 某些情况下,for...in循环会以任意顺序遍历键名。
    for...of:
        + 有着同for...in一样的简洁语法
        + 可以与break、continue和return配合使用
        + 提供了遍历所有数据结构的统一操作接口
class
1.ES6 的class可以看作只是一个语法糖,绝大多数功能ess5都能做到,语法更清晰
es5:
function Point(x, y) {
    this.x = x;
    this.y = y;
}
Point.prototype.toString = function () {
    return '(' + this.x + ', ' + this.y + ')'
}
var p = new Point(1, 2);

es6(一个语法糖):
class Point { // 构造函数
    // 构造方法
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ', ' + this.y + ')';
    }
}

class Point {
    constructor() {
        // ...
    }
    toString() {
        // ...
    }
    toValue() {
        // ...
    }
}

等同于

Point.prototype = {
    constructor() {},
    toString() {},
    toValue() {},
};

class B {}
let b = new B();

2. 和es5一致的地方

    B.prototype.constructor === b.constructor === B

3. 和es5不一致的地方
    (1)
        es6:类的内部所有定义的方法,都是不可枚举的
        class Point {
            constructor(x, y) {
                // ...
            }
            toString() {
                // ...
            }
        }
        Object.keys(Point.prototype)
        // []
        Object.getOwnPropertyNames(Point.prototype)
        // ["constructor","toString"]
    
    es5:可枚举的
        var Point = function (x, y) {
          // ...
        };
        Point.prototype.toString = function() {
          // ...
        };
        
        Object.keys(Point.prototype)
        // ["toString"]
        Object.getOwnPropertyNames(Point.prototype)
        // ["constructor","toString"]
    
    (2)
    类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别
4.constructor 方法
    constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
    一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
    
·
    

1. 新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法
2. 类的内部所有定义的方法,都是不可枚举的,与es5是不一致的
3. 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行
类不存在变量提升
es6和es5继承的区别
class的所以方法都是不可枚举的
class声明内部采用严格模式
class会声明提升,但不会初始化赋值,提前用会进入暂时性死区
class内部无法重写类名
必须使用new调用class
1class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。
    const bar = new Bar(); // it's ok
    function Bar() {
      this.bar = 42;
    }

    const foo = new Foo(); // ReferenceError: Foo is not defined
    class Foo {
      constructor() {
        this.foo = 42;
      }
    }
2class 声明内部会启用严格模式。
    // 引用一个未声明的变量
    function Bar() {
      baz = 42; // it's ok
    }
    const bar = new Bar();

    class Foo {
      constructor() {
        fol = 42; // ReferenceError: fol is not defined
      }
    }
    const foo = new Foo();
3class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
    // 引用一个未声明的变量
    function Bar() {
      this.bar = 42;
    }
    Bar.answer = function() {
      return 42;
    };
    Bar.prototype.print = function() {
      console.log(this.bar);
    };
    const barKeys = Object.keys(Bar); // ['answer']
    const barProtoKeys = Object.keys(Bar.prototype); // ['print']

    class Foo {
      constructor() {
        this.foo = 42;
      }
      static answer() {
        return 42;
      }
      print() {
        console.log(this.foo);
      }
    }
    const fooKeys = Object.keys(Foo); // []
    const fooProtoKeys = Object.keys(Foo.prototype); // []
4class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
    function Bar() {
      this.bar = 42;
    }
    Bar.prototype.print = function() {
      console.log(this.bar);
    };

    const bar = new Bar();
    const barPrint = new bar.print(); // it's ok

    class Foo {
      constructor() {
        this.foo = 42;
      }
      print() {
        console.log(this.foo);
      }
    }
    const foo = new Foo();
    const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
5、必须使用 new 调用 classfunction Bar() {
      this.bar = 42;
    }
    const bar = Bar(); // it's ok

    class Foo {
      constructor() {
        this.foo = 42;
      }
    }
    const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
6class 内部无法重写类名。
    function Bar() {
      Bar = 'Baz'; // it's ok
      this.bar = 42;
    }
    const bar = new Bar();
    // Bar: 'Baz'
    // bar: Bar {bar: 42}  

    class Foo {
      constructor() {
        this.foo = 42;
        Foo = 'Fol'; // TypeError: Assignment to constant variable
      }
    }
    const foo = new Foo();
    Foo = 'Fol'; // it's ok

解决浏览器兼容:babel + ployfill
babel: 转换浏览器不支持的语法,es6–>es5
ployfill: 转换浏览器不支持的API

babel下的es6兼容性和规范
https://www.cnblogs.com/chris-oil/p/5931180.html

零散知识点

  • ||、&&都是短路运算符
  • !! 运算符的作用:可以强制将右侧的值转为布尔型
    • console.log(!!null); // false
    • console.log(!!undefined); // false
    • console.log(!!‘’); // false
    • console.log(!!0); // false
    • console.log(!!NaN); // false
    • console.log(!!’ '); // true
    • console.log(!!{}); // true
    • console.log(!![]); // true
    • console.log(!!1); // true
    • console.log(!![].length); // false
;