前端面试题-函数继承,绑定,科里化,原型和原型链,异步加载,async,defer,惰性载入函数,私有变量,面向对象,继承深拷贝和浅拷贝

函数绑定

函数绑定要创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。我们来看段代码:

typescript 复制代码
  EventUtil: {
    /*
     * desc: 视情况而定使用不同的事件处理程序
     * @param : element,要操作的元素
     * @param : type,事件名称
     * @param : handler,事件处理程序函数
    */
    addHandler: function (element, type, handler) {
      if (element.addEventListener) { // DOM2级
        element.addEventListener(type, handler, false)
      } else if (element.attachEvent) { // IE级
        element.attachEvent(`on${type}`, handler)
      } else {
        element[`on${type}`] = handler // DOM0级
      }
    }
  }

  var handler = {
    message: 'I am message',

    handleClick: function () {
      console.log(this.message)
    }
  }

  var btn = document.getElementById('click-btn')
  EventUtil.addHandler(btn, 'click', handler.handleClick)

正常来讲,当我们按下这个按钮的时候,就调用 handler.handleClick() 函数,打印 ' I am message ',但是实际上显示的是 " undefined ",为什么呢?

问题就在于没有保存 handler.handleClick() 的环境,所以 this 对象最后是指向了 DOM按钮 而不是 handler对象。所以通过一个闭包,来解决这个问题,不知道闭包的点击这里!!!

javascript 复制代码
  var handler = {
    message: 'I am message',

    handleClick: function () {
      console.log(this.message)
    }
  }
  
  var btn = document.getElementById('click-btn')
  EventUtil.addHandler(btn, 'click', function (event) {
    handler.handleClick(event)
  })

这个解决方案就是在 onclick 事件处理程序中,使用了一个闭包直接调用 handler.handleClick(),在 JavaScript 库中实现了一个可以将函数绑定到执行环境的函数中,这个函数叫做 bind()

bind()函数是在 ES5 才被加入;它可能无法在所有浏览器上运行。这就需要我们自己实现bind()函数了。自己实现一个 bind() ? 先不急,我们再来看一个概念,叫做: 函数柯里化 !!!

函数柯里化

它用于创建已经设置好了一个或多 个参数的函数。函数柯里化的基本方法和函数绑定是一样的: 使用一个闭包返回一个函数。两者的区别在于 : 当函数被调用时,返回的函数还需要设置一些传入的参数

javascript 复制代码
  function add (num1, num2) {
    return num1 + num2
  }

  function curriedAdd(num2) {
    return add(5, num2)
  }

  console.log(add(2, 3))        // 5
  console.log(curriedAdd(3))    // 8

这段代码定义了两个函数: add() 和 curriedAdd()。后者本质上是在任何情况下第一个参数为 5 的 add()版本。尽管从技术上来说 curriedAdd()并非柯里化的函数,但它很好地展示了其概念。

柯里化函数通常由以下步骤动态创建 : 调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式。

javascript 复制代码
  function curry (fn) {
    var args = Array.prototype.slice.call(arguments, 1)
    return function () {
      var innerArgs = Array.prototype.slice.call(arguments)
      var finalArgs = args.concat(innerArgs)
      return fn.apply(null, finalArgs)
    }
  }

curry() 函数的主要工作就是将被返回函数的参数进行排序。curry() 的第一个参数是要进行柯里化的函数,其他参数是要传入的值。为了获取第一个参数之后的所有参数,在 arguments 对象上调用了 slice()方法,并传入参数 1 表示被返回的数组包含从第二个参数开始的所有参数。

然后 args 数组包含了来自外部函数的参数。在内部函数中,创建了 innerArgs 数组用来存放所有传入的参数(又一次用到了 slice())。有了存放来自外部函数和内部函数的参数数组后,就可以使用 concat() 方法将它们组合为 finalArgs,然后使用 apply() 将结果传递给该函数。注意这个函数并没有考虑到执行环境,所以调用 apply()时第一个参数是 null。curry()函数可以按以下方式应用

csharp 复制代码
  function add (num1, num2) {
    return num1 + num2
  }

  var curriedAdd = curry(add, 5)
  console.log(curriedAdd(3)) // 8

在这个例子中,创建了第一个参数绑定为 5 的 add()的柯里化版本。当调用 curriedAdd()并传入 3 时,3 会成为 add() 的第二个参数,同时第一个参数依然是 5,最后结果便是和 8。你也可以像下面例子这样给出所有的函数参数

csharp 复制代码
  function add (num1, num2) {
    return num1 + num2
  }

  var curriedAdd = curry(add, 5, 12)
  console.log(curriedAdd())  // 17

在这里,柯里化的 add()函数两个参数都提供了,所以以后就无需再传递它们了。

结合函数柯里化的情况,实现一个_bind()函数

javascript 复制代码
  // 写法一
  Function.prototype._bind = function (context) {
    var args = Array.prototype.slice.call(arguments, 1) // 表示被返回的数组包含从第二个参数开始的所有参数。
    var self = this // 保存this,即调用_bind方法的目标函数
    return function () {
      var innerArgs = Array.prototype.slice.call(arguments)
      var finalArgs = args.concat(innerArgs)
      return self.apply(context, finalArgs)
    }
  }

  // 写法二
  function _bind (fn, context) {
    var args = Array.prototype.slice.call(arguments, 2) // 表示被返回的数组包含从第三个参数开始的所有参数。
    return function () {
      var innerArgs = Array.prototype.slice.call(arguments)
      var finalArgs = args.concat(innerArgs)
      return fn.apply(context, finalArgs)
    }
  }

所以这时候我们通过绑定函数给之前的例子重写一下,就能正常了~

javascript 复制代码
  var handler = {
    message: 'I am message',

    handleClick: function (name, event) {
      console.log(this.message + ':' + name + ':' + event.type)
    }
  }
  
  var btn = document.getElementById('click-btn')
  EventUtil.addHandler(btn, 'click', _bind(handler.handleClick, handler, 'btn'))

在这个更新过的例子中,handler.handleClick() 方法接受了两个参数: 要处理的元素的名字和 event 对象。作为第三个参数传递给 bind()函数的名字,又被传递给了 handler.handleClick(), 而 handler.handleClick() 也会同时接收到 event 对象。 调用另外一个函数并且为他传入要科里化的函数和必要的参数,

curry函数的主要工作就是讲被返回的函数的参数进行排序,curry的第一个参数就是要进行科里化的函数,其他的参数是要传入的值,

原型与原型链

构造函数 (constructor,内置的默认属性)

ini 复制代码
    1 : 例子
    
    function Person(name, age, job) {
      this.name = name;
      this.age = age;
      this.job = job;
      this.sayName = function() { 
        alert(this.name) 
      } 
    }
    
    var person1 = new Person('Zaxlct', 28, 'Software Engineer');
    var person2 = new Person('Mick', 23, 'Doctor');

    // 上面的例子中 person1 和 person2 都是Person的实例
        
    console.log(person1.constructor == Person)      // true
    console.log(person2.constructor == Person)      // true

    // 直接打印person1和person2对象,就会发现并没有发现有constructor属性
    // 那为什么person1.constructor == Person 这个会是true,实际是因为在person1中没有找到constructor属性
    
    // 顺着__proto__往上,找到了Person.prototype,而在这里才找到的constructor,而这个constructor是指向Person的,所以结果才会是true,但是这并不能说是实例上有一个constructor属性

记住 :

  • person1 和 person2 都是 构造函数 Person 的实例
  • 实例的构造函数属性都指向构造函数

原型对象

每定义一个对象时候,对象中都会包含一些预定义的属性,每个函数对象都会有一个prototype属性,这个属性指向函数的原型对象。

javascript 复制代码
    function Person() {}
      Person.prototype.name = '彭道宽'
      Person.prototype.age  = 28
      Person.prototype.job  = 'Web Engineer'
      Person.prototype.sayName = function() {
      alert(this.name)
    }
        
    var person1 = new Person()
    person1.sayName() // '彭道宽'

    var person2 = new Person()
    person2.sayName() // '彭道宽'

    console.log(person1.sayName == person2.sayName); //true
  • 每一个对象都有__proto__属性, 但是只有函数对象才有 prototype 属性
javascript 复制代码
    什么是函数对象 ?
    
    凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象,下面例子都是函数对象。。。

    var f1 = function () {
      name : '彭道宽',
      age : 18
    }

    var f2 = new Function('彭道宽', 18)

那什么是原型对象呢 ?

原型对象,顾名思义,它就是一个普通对象。原型对象就是 Person.prototype ,如果你还是害怕它,那就把它想想成一个字母 A: var A = Person.prototype

现在我们给A添加四个属性,name 、age 、 job 、 say,其实它还有一个默认的属性就是constructor。

这么说吧 : A 有一个默认的 constructor 属性,这个属性是一个指针,指向 Person。即:

ini 复制代码
    Person.prototype.constructor = Person

是不是和上边说的 ?

kotlin 复制代码
    // 上边
    person1.constructor == Person                // true

    // 这里
    Person.prototype.constructor == Person       // true

    // 这里为什么person1会constructor属性?因为person1 是 Person的实例。实际上并没有constructor,在找的时候找不到constructor。

    // 于是顺着__proto__往上找,由于person1是Person的实例,于是找到了Person.prototype,在这里找到了cosntructor,所以上边的公式才成立

    // 注意
    person1.constructor == Person.prototype.constructor // false
    // person1.constructor 和 Person.prototype.constructor 是指针属性,只是同时指向 Person,并不是等于Person,所以是错误的

那 Person.prototype 为什么有 constructor 属性??同理, Person.prototype(也就是A),也是Person 的实例。

javascript 复制代码
    // 第一步
    let A = {}

    // 第二步
    A.__proto__ = Person.prototype

    // 第三步
    Person.call(A)

    // 第四步
   return A

    // 原型对象(Person.prototype)是 构造函数(Person)的一个实例。
    Person.prototype = A

原型对象其实就是普通对象(但 Function.prototype 除外,它是函数对象,但它很特殊,他没有prototype属性(前面说到函数对象都有prototype属性))

javascript 复制代码
    function Person(){};
    console.log(Person.prototype) //Person{}
    console.log(typeof Person.prototype) //Object
    console.log(typeof Function.prototype) // Function,这个特殊
    console.log(typeof Object.prototype) // Object
    console.log(typeof Function.prototype.prototype) //undefined

proto

JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__ 的内置属性,用于指向创建它的构造函数的原型对象。

对象 person1 有一个 __proto__属性,创建它的构造函数是 Person,构造函数的原型对象是 Person.prototype ,所以:

ini 复制代码
    person1.__proto__  == Person.prototype

每一个对象都有_proto_属性,但是只有函数对象才有prototype属性 new Function()创建出来的对象都是函数对象 ------proto_是原型,每个对象都存在原型,person1之所以有constructor属性,是因为他是Person的实力new出来的对象 ------proto_的内置属性,指向创建他的构造函数的原型对象

从这个图中可得到 :

ini 复制代码
    Person.prototype.constructor = Person

    person1.__proto__ == Person.prototype

    person1.constructor == Person
    

注意 : 这个连接存在于实例(person1)与构造函数(Person)的原型对象(Person.prototype)之间,而不是存在于实例(person1)与构造函数(Person)之间。

总结一下

markdown 复制代码
    // 概念

    1 : Person 构造函数

    2 : var person1 = new Person() , person1 是实例

    3 : prototype是原型对象,只有Function Object(函数对象) 才存在

    4 : __proto__是原型,每个对象都存在原型

    5 : person1之所以有constructor属性,是因为它是Person的实例,它是new出来的对象,person1 的 constructor指向Person


    // 公式

    1 : Person.prototype.constructor == Person

    2 : person1.constructor == Person

    3 : person1.__proto__ == Person.prototype

原型链

javascript 复制代码
    // 题目
    1 : person1.__proto__ 是什么?
    2 : Person.__proto__ 是什么?
    3 : Person.prototype.__proto__ 是什么?
    4 : Object.__proto__ 是什么?
    5 : Object.prototype.__proto__ 是什么?

    // 答案
    1 : person1.__proto__ === Person.prototype (person1的构造函数Person)

    2 : Person.__proto__ === Function.prototpye (Person的构造函数Function)

    3 : Person.protyotype是一个普通对象,因为一个普通对象的构造函数都是Object
        所以 Person.prototype.__proto__  === Object.prototype

    4 : Object.__proto__ === Function.prototpye (Object的构造函数Function)

    5 : Object.prototype 也有__proto__属性,但是它比较特殊,是null,null处于原型链的顶端。所以 : Object.prototype.__proto__ === null

注意 :

  • 原型链的形成是真正是靠__proto__ 而非prototype

自己写一下?

javascript 复制代码
    function Person() {

    }

    var p1 = new Person()

    // 总结公式
    1 : p1.constructor = Person
    
    2 : Person.prototype.constructor = Person

    3 : p1.__proto__ = Person.prototype

    4 : Person.__proto__ = Function.prototype

    5 : Person.constructor = Function

    6 : Person.prototype.__proto__ = Object.prototype

    7 : Object.__proto__ = Function.prototype
        // Object 是函对象,是通new Function()创建的,所以Object.__proto__指向Function.prototype

    8 :Function.prototype.__proto__ = Object.prototype
    
    9 : Object.prototype.__proto__ = null

来个题?

javascript 复制代码
    var FunctionExample = function () {}

    Object.prototype.a = function() {}

    Function.prototype.b = function() {}

    var f = new FunctionExample()

    // 这时候f能否访问到a和b

    // 所有普通对象都源于这个Object.prototype对象,只要是对象,都能访问到a
    // 而f通过new 关键词进行函数调用,之后无论如何都会返回一个与FunctionExample关联的普通对象(因为不是通过函数构造创建的对象,所以不是函数对象,如果不是函数对象,不存在prototype,也就取不到b了)
    // 而取b我们可通过 f.constructor.b就能访问到b,因为 f.constructor == FunctionExample

    console.log(f) // FunctionExample {}
    console.log(f.constructor)  // [Function: FunctionExample]
    console.log(FunctionExample.prototype) // FunctionExample {}, 其实可以理解成FunctionExample.prototype就是一个实例
    console.log(FunctionExample.prototype.constructor) // [Function: FunctionExample]
    console.log(f.__proto__) // FunctionExample {} , 可以这么理解,实例的proto指向它的构造函数的原型对象,也就是f.__proto__ == FunctionExample.prototype
    console.log(f.constructor.b) // Function,因为f.constructor指向 FunctionExample, 而 FunctionExample.prototype相当是Function的一个实例,所以在Function.prototype上有个b函数,FunctionExample照样可以访问的到
    console.log(f.constructor.prototype.__proto__) // { a: [Function] } 可以访问到a函数,因为f.constructor.prototype其实就是等于FunctionExample {},而每个对象都有个__proto__属性,Function.prototype.__proto__ == Object.prototype,所以也能访问到a方法

再来两个题

javascript 复制代码
  function SuperType() {
    this.colors = ['red', 'yellow']
  }

  function SubType() {
    
  }

  // 继承了SuperType
  SubType.prototype = new SuperType()

  var instance1 = new SubType() // intance.constructor = SuperType
  instance1.colors.push('black')
  console.log(instance1.colors) // ['red', 'yellow', 'black']

  var instance2 = new SubType() 
  console.log(instance2.colors) // ['red', 'yellow', 'black']

  // 理解一下原型和原型链
  console.log(instance1.constructor) // SuperType
  console.log(SubType.prototype.constructor) // SuperType
  console.log(SubType.prototype.__proto__ == SuperType.prototype) // true
  console.log(instance1.__proto__ == SubType.prototype) // true
  console.log(SubType.__proto__ == SuperType.prototype) // false
  console.log(SubType.__proto__ == Function.prototype) // true
  console.log(SuperType.prototype.constructor == SuperType) // true
  console.log(SuperType.__proto__ == Function.prototype) // true
  console.log(SuperType.prototype.__proto__ == Object.prototype) // true 

  // 为什么instance1.constructor = SuperType ? 为什么 SubType.prototype.constructor = SuperType ? 
javascript 复制代码
  function SuperType() {
    this.colors = ['red', 'yellow']
  }

  function SubType() {
    // 继承了SuperType
    SuperType.call(this)
  }

  var instance1 = new SubType()
  instance1.colors.push('black')
  console.log(instance1.colors) // ['red', 'yellow', 'black']

  var instance2 = new SubType() 
  console.log(instance2.colors) // ['red', 'yellow']

  // 思考一哈?
  console.log(instance1.constructor) // SubType
  console.log(SubType.prototype.constructor) // SubType
  console.log(SubType.prototype.__proto__) // {}
  console.log(SubType.prototype.__proto__ == SuperType.prototype) // false
  console.log(SubType.prototype.__proto__ == Object.prototype) // true
  console.log(instance1.__proto__ == SubType.prototype) // true
  console.log(SubType.__proto__ == SuperType.prototype) // false
  console.log(SubType.__proto__ == Function.prototype) // true
  console.log(SuperType.prototype.constructor == SuperType) // true
  console.log(SuperType.__proto__ == Function.prototype) // true
  console.log(SuperType.prototype.__proto__ == Object.prototype) // true 

详情看这里啊,《JavaScript高级程序设计 第三版》中: 继承

异步加载 js, async 和 defer

有三种 : defer 、 async 、 动态创建 script 标签 、 按需异步载入 js

  • async : 并行加载脚本文件,下载完毕立即解释执行代码,不会按照页面上的 script 顺序执行。
  • defer : 并行下载 js,会按照页面上的 script 标签的顺序执行,然后在文档解析完成之后执行脚本。

异步加载 js, async 和 defer

有三种 : defer 、 async 、 动态创建 script 标签 、 按需异步载入 js

  • async : 并行加载脚本文件,下载完毕立即解释执行代码,不会按照页面上的 script 顺序执行。
  • defer : 并行下载 js,会按照页面上的 script 标签的顺序执行,然后在文档解析完成之后执行脚本。

解析

<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,"立即"指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

<script async src="script.js"></script>

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

<script defer src="myscript.js"></script>

有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

defer文档解析完成以后执行脚本,async并行加载脚本文件,下载完毕立即解释执行代码,

DOMContetLoaded事件触发代表初始的html被完全加载和解析,

defer加载后续文档元素的过程和script.js的加载并行进行一步,但是script.js的执行要在所有的元素解析完车鞥之后DOMContentLoaded事件触发之前完成,## 惰性载入函数

因为浏览器之间行为的差异,多数 JavaScript 代码包含了大量的 if 语句,将执行引导到正确的代码中。比如说,你调用某一函数判断a的时候,总会走一些其他的分支,比如说 if (a<3) , else if (a > 20),在你第一次执行该函数的时候就知道 a = 10,那么第二次,第三次执行该函数,就没必要走这些分支了。举个例子,打卡第十五天里的 createXHR() 函数:

javascript 复制代码
  function createXHR () {
    if (typeof XMLHttpRequest != 'undefined') {
      return new XMLHttpRequest()  // 返回IE7及更高版本
    } else if (typeof ActiveObject != 'undefined') { // 适用于IE7之前的版本
      if (typeof arguments.callee.activeXString != 'String') {
        var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
        var i, len
        for (i = 0; i < versions.length; i++) {
          try {
            new ActiveObject(versions[i])
            arguments.callee.activeXString = versions[i]
            break
          } catch (error) {
            // 跳过
          }
        }
      }
      return new ActiveObject(arguments.callee.activeXString)
    } else {
      throw new Error('NO XHR object available')
    }
  }

每次调用 createXHR()的时候,它都要对浏览器所支持的能力仔细检查。

首先检查内置的 XHR, 然后测试有没有基于 ActiveX 的 XHR,最后如果都没有发现的话就抛出一个错误。每次调用该函数都是这样,即使每次调用时分支的结果都不变: 如果浏览器支持内置 XHR,那么它就一直支持了,那么这 种测试就变得没必要了。

即使只有一个 if 语句的代码,也肯定要比没有 if 语句的慢,所以如果 if 语句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为 惰性载入 的技巧。惰性载入表示函数执行的分支仅会发生一次。

有两种实现惰性载入的方式,第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了

javascript 复制代码
  function createXHR () {
    if (typeof XMLHttpRequest != 'undefined') {
      createXHR = function () {
        return new XHLHttpRequest()
      }
    } else if (typeof ActiveObject != 'undefined') {
      createXHR = function () {
        if (typeof arguments.callee.activeXString != 'String') {
          var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
          var i, len
          for (i = 0; i < versions.length; i++) {
            try {
              new ActiveObject(versions[i])
              arguments.callee.activeXString = versions[i]
              break
            } catch (error) {
              // 跳过
            }
          }
        }
        return new ActiveObject(arguments.callee.activeXString)
      }
    } else {
      createXHR = function () {
        throw new Error('NO XHR object available')
      }
    }

    return createXHR()
  }

在这个惰性载入的 createXHR()中,if 语句的每一个分支都会为 createXHR 变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用 createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行 if 语句了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。(其实就是通过匿名函数立即执行)

javascript 复制代码
  var createXHR = (function() {
    if (typeof XMLHttpRequest != 'undefined') {
      return function () {
        return new XHLHttpRequest()
      }
    } else if (typeof ActiveObject != 'undefined') {
      return function () {
        if (typeof arguments.callee.activeXString != 'String') {
          var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
          var i, len
          for (i = 0; i < versions.length; i++) {
            try {
              new ActiveObject(versions[i])
              arguments.callee.activeXString = versions[i]
              break
            } catch (error) {
              // 跳过
            }
          }
        }
        return new ActiveObject(arguments.callee.activeXString)
      }
    } else {
      return function () {
        throw new Error('NO XHR object available')
      }
    }
  })()

惰性载入函数

因为浏览器之间行为的差异,多数 JavaScript 代码包含了大量的 if 语句,将执行引导到正确的代码中。比如说,你调用某一函数判断a的时候,总会走一些其他的分支,比如说 if (a<3) , else if (a > 20),在你第一次执行该函数的时候就知道 a = 10,那么第二次,第三次执行该函数,就没必要走这些分支了。举个例子,打卡第十五天里的 createXHR() 函数:

javascript 复制代码
  function createXHR () {
    if (typeof XMLHttpRequest != 'undefined') {
      return new XMLHttpRequest()  // 返回IE7及更高版本
    } else if (typeof ActiveObject != 'undefined') { // 适用于IE7之前的版本
      if (typeof arguments.callee.activeXString != 'String') {
        var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
        var i, len
        for (i = 0; i < versions.length; i++) {
          try {
            new ActiveObject(versions[i])
            arguments.callee.activeXString = versions[i]
            break
          } catch (error) {
            // 跳过
          }
        }
      }
      return new ActiveObject(arguments.callee.activeXString)
    } else {
      throw new Error('NO XHR object available')
    }
  }

每次调用 createXHR()的时候,它都要对浏览器所支持的能力仔细检查。

首先检查内置的 XHR, 然后测试有没有基于 ActiveX 的 XHR,最后如果都没有发现的话就抛出一个错误。每次调用该函数都是这样,即使每次调用时分支的结果都不变: 如果浏览器支持内置 XHR,那么它就一直支持了,那么这 种测试就变得没必要了。

即使只有一个 if 语句的代码,也肯定要比没有 if 语句的慢,所以如果 if 语句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为 惰性载入 的技巧。惰性载入表示函数执行的分支仅会发生一次。

有两种实现惰性载入的方式,第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了

javascript 复制代码
  function createXHR () {
    if (typeof XMLHttpRequest != 'undefined') {
      createXHR = function () {
        return new XHLHttpRequest()
      }
    } else if (typeof ActiveObject != 'undefined') {
      createXHR = function () {
        if (typeof arguments.callee.activeXString != 'String') {
          var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
          var i, len
          for (i = 0; i < versions.length; i++) {
            try {
              new ActiveObject(versions[i])
              arguments.callee.activeXString = versions[i]
              break
            } catch (error) {
              // 跳过
            }
          }
        }
        return new ActiveObject(arguments.callee.activeXString)
      }
    } else {
      createXHR = function () {
        throw new Error('NO XHR object available')
      }
    }

    return createXHR()
  }

在这个惰性载入的 createXHR()中,if 语句的每一个分支都会为 createXHR 变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用 createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行 if 语句了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。(其实就是通过匿名函数立即执行)

javascript 复制代码
  var createXHR = (function() {
    if (typeof XMLHttpRequest != 'undefined') {
      return function () {
        return new XHLHttpRequest()
      }
    } else if (typeof ActiveObject != 'undefined') {
      return function () {
        if (typeof arguments.callee.activeXString != 'String') {
          var version = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"]
          var i, len
          for (i = 0; i < versions.length; i++) {
            try {
              new ActiveObject(versions[i])
              arguments.callee.activeXString = versions[i]
              break
            } catch (error) {
              // 跳过
            }
          }
        }
        return new ActiveObject(arguments.callee.activeXString)
      }
    } else {
      return function () {
        throw new Error('NO XHR object available')
      }
    }
  })()

首先检查内置的xhr,测试有没有基于activeX的xhr,最后如果米有发现的话就跑出一个错误,

深拷贝和浅拷贝的问题

ini 复制代码
let obj1 = {
  age: 18
}

let obj2 = obj1
obj1.age = 19
console.log(obj2.age) // 19

我们可以看到,当我们把一个变量赋值一个对象,那么两者的值会同一个引用,改变 obj1,也同时会改变 ob,但是在实际开发中,我们并不希望这样子,这时候就需要通过浅拷贝来解决这个问题

浅拷贝 ( Object.assign() )

ini 复制代码
    通过 Object.assign() 来解决这个问题

    let obj1 = {
        age : 18
    }

    let obj2 = Object.assign({}, obj1)
    obj1.age = 19
    console.log(obj2.age)   // 18

浅拷贝只能解决第一层问题,但如果 obj1 里还有个 bank 是个对象,里面还有值呢?那么又回到了之前说的,两者共享相同的引用。所以这里使用深拷贝

深拷贝 ( JSON.pares(JSON.stringify(object)) )

typescript 复制代码
    通过 JSON.pares(JSON.stringify(object)) 来解决这个问题

    let obj1 = {
        age : 18,
        bank : {
            b1 : '中国招商银行',
        }
    }

    let obj2 = JSON.parse(JSON.stringify(obj1))
    obj1.bank.b1 = '中国建设银行'
    console.log(obj2.bank.b1)       // 中国招商银行

    但是这种方法有局限性
    · 会忽略undefined
    · 不能序列化函数
    · 不能解循环引用对象

    当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。(自行百度)
javascript 复制代码
    在遇到函数或者 undefined 的时候,该对象也不能正常的序列化

    let obj = {
        name: unfefined,
        say: function () {},
        school: 'HNUST'
    }

    let res = JSON.parse(JSON.stringify(obj))   // {school: 'HNUST'}

    // 该方法会忽略掉函数和 undefined ,所以序列化后就只有school

你能不能用代码实现拷贝?

ini 复制代码
function deepClone(data) {
  let type = Object.prototype.toString.call(data)
  let obj
  if (type === '[object Array]') {
    obj = []
  } else if (type === '[object Object]') {
    obj = {}
  } else {
    return data
  }

  if (type === '[object Array]') {
    for (let i = 0; i < data.length; i++) {
      obj.push(deepClone(data[i]))
    }
  } else if (type === '[object Object]') {
    for (let key in data) {
      obj[key] = deepClone(data[key])
    }
  }
  return obj
}

var data = {
  name: '跑得快',
  age: 20,
  school: {
    name: '湖南科技大学',
    major: '软件工程',
    class: {
      id: 2,
      student: [1, 2, 3, 4, 6]
    }
  }
}

console.log(deepClone(data))

深拷贝和浅拷贝的问题

ini 复制代码
let obj1 = {
  age: 18
}

let obj2 = obj1
obj1.age = 19
console.log(obj2.age) // 19

我们可以看到,当我们把一个变量赋值一个对象,那么两者的值会同一个引用,改变 obj1,也同时会改变 ob,但是在实际开发中,我们并不希望这样子,这时候就需要通过浅拷贝来解决这个问题

浅拷贝 ( Object.assign() )

ini 复制代码
    通过 Object.assign() 来解决这个问题

    let obj1 = {
        age : 18
    }

    let obj2 = Object.assign({}, obj1)
    obj1.age = 19
    console.log(obj2.age)   // 18

浅拷贝只能解决第一层问题,但如果 obj1 里还有个 bank 是个对象,里面还有值呢?那么又回到了之前说的,两者共享相同的引用。所以这里使用深拷贝

深拷贝 ( JSON.pares(JSON.stringify(object)) )

typescript 复制代码
    通过 JSON.pares(JSON.stringify(object)) 来解决这个问题

    let obj1 = {
        age : 18,
        bank : {
            b1 : '中国招商银行',
        }
    }

    let obj2 = JSON.parse(JSON.stringify(obj1))
    obj1.bank.b1 = '中国建设银行'
    console.log(obj2.bank.b1)       // 中国招商银行

    但是这种方法有局限性
    · 会忽略undefined
    · 不能序列化函数
    · 不能解循环引用对象

    当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数。(自行百度)
javascript 复制代码
    在遇到函数或者 undefined 的时候,该对象也不能正常的序列化

    let obj = {
        name: unfefined,
        say: function () {},
        school: 'HNUST'
    }

    let res = JSON.parse(JSON.stringify(obj))   // {school: 'HNUST'}

    // 该方法会忽略掉函数和 undefined ,所以序列化后就只有school

你能不能用代码实现拷贝?

ini 复制代码
function deepClone(data) {
  let type = Object.prototype.toString.call(data)
  let obj
  if (type === '[object Array]') {
    obj = []
  } else if (type === '[object Object]') {
    obj = {}
  } else {
    return data
  }

  if (type === '[object Array]') {
    for (let i = 0; i < data.length; i++) {
      obj.push(deepClone(data[i]))
    }
  } else if (type === '[object Object]') {
    for (let key in data) {
      obj[key] = deepClone(data[key])
    }
  }
  return obj
}

var data = {
  name: '跑得快',
  age: 20,
  school: {
    name: '湖南科技大学',
    major: '软件工程',
    class: {
      id: 2,
      student: [1, 2, 3, 4, 6]
    }
  }
}

console.log(deepClone(data))

JSON。parese(JSON.stringfyobject)_ JSO。parse(JSON.stringfy(objecy)); 局限性: 不能序列化函数,忽略undefined,循环引用对象

私有变量

严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量

私有变量包括: 函数的参数局部变量和在函数内部定义的其他函数,比如:

bash 复制代码
  function add(num1, num2){
    var sum = num1 + num2;
    return sum;
  }

在这个函数内部,有 3 个私有变量:num1、num2 和 sum。在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。 而利用这一点,就可以创建用于访问私有变量的公有方法。

我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下

javascript 复制代码
  function MyObject(){
    var privateVariable = 10

    function privateFunction () {
      return false
    }

    this.publicMethod = function (){
      privateVariable++
      return privateFunction()
    }
  }

这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数 。(说白了,特权方法就是闭包,而利用闭包的作用域链,可以访问到外部函数的变量和方法)。对这个例子而言,变量 privateVariable 和函数 privateFunction()只能通过特 权方法 publicMethod()来访问。在创建 MyObject 的实例后,除了使用 publicMethod()这一个途 径外,没有任何办法可以直接访问 privateVariable 和 privateFunction()。

静态私有变量

javascript 复制代码
  (function(){
    //私有变量和私有函数
    var privateVariable = 10

    function privateFunction() {
      return false;
    }

    //构造函数
    MyObject = function(){ }

    //公有/特权方法
    MyObject.prototype.publicMethod = function(){
      privateVariable++
      return privateFunction()
    }
  })()

记住 : 初始化未经声明的变量,总是会创建一个全局变量

  • 这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法
  • 在私有作用域中, 首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法
  • 这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明 MyObject 时使用 var 关键字
  • 因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下 给未经声明的变量赋值会导致错误

这个模式与在构造函数中定义特权方法的主要区别: 就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的, 而道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。 按照惯例,JavaScript 是以对象字面量的方式来创建单例对象的。

模块模式通过为单例添加私有变量和特权方法能够使其得到增强

php 复制代码
  var singleton = function () {

    //私有变量和私有函数
    var privateVariable = 10
    function privateFunction () {
      return false
    }

    // 特权/公有方法和属性
    return {
      publicProperty: true,
      publicMethod: function () {
        privateVariable++
        return privateFunction()
      }
    }
  }()

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。 然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那 些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。来看下面的例子

javascript 复制代码
  var singleton = function () {

    //私有变量和私有函数
    var privateVariable = 10
    function privateFunction () {
      return false
    }

    // 创建对象
    var obj = new Object()

    // 特权/公有方法和属性
    obj.publicProperty = true
    obj.publicMethod - function () {
      privateVariable++
      return privateFunction()
    }
    
    // 返回这个对象
    return obj
  }()

特权方法作为闭包有权访问在构造函数当中定义的所有的变量和函数 特权方法是闭包, 模块模式通过为单例添加私有变量和特权方法能够使得得到增强 对象字面量定义的是单例的公共接口 这种增强的模块模式适用于那些单例必须是某种类型的实例

面向对象

如何声明一个类 ?

ES5 中,还没有类的概念,而是通过函数来声明,到了 ES6,有了 class 关键词,则通过 class 来声明

javascript 复制代码
// ES5
var Animal = function() {
  this.name = 'Animal'
}

// ES6
class Animal {
  constructor() {
    this.name = 'Animal'
  }
}

如何创建对象 ?

  • 字面量对象
  • 显示的构造函数
  • Object.create
javascript 复制代码
// 第一种方式: 字面量
var obj1 = {
  name: '彭道宽'
}
var obj2 = new Object({
  name: '彭道宽'
})

// 第二种方式: 构造函数
var Parent = function() {
  this.name = name
}
var child = new Parent('彭道宽')

// 第三种方式: Object.create
var Parent = {
  name: '彭道宽'
}
var obj4 = Object.create(Parent)

ES6 的 Class

基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

javascript 复制代码
// ES5
function Point(x, y) {
  this.x = x
  this.y = y
}

Point.prototype.toString = function() {
  return `(${this.x}, ${this.y})` // (x, y)
}

var point = new Point(1, 2)
point.toString() // (1, 2)

// ES6 中利用 class 定义类
class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  toString() {
    return `(${this.x}, ${this.y})`
  }
}

var point = new Point(1, 2)
point.toString() // (1, 2)

上面代码定义了一个"类",可以看到里面有一个 constructor方法,这就是构造方法,而 this 关键字则代表实例对象。也就是说,ES5 的构造函数 Point,对应 ES6 的 Point 类的构造方法。

javascript 复制代码
class Point {
  // ...
}

console.log(typeof Point) // 'function'
console.log(Point === Point.prototype.constructor) // true

类的数据类型就是函数,类本身就指向构造函数。 构造函数的 prototype 属性,在 ES6 的 "类" 上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。

javascript 复制代码
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于下边的代码
Point.prototype.constructor = function() {}
Point.prototype.toString = function() {}
Point.prototype.toValue = function() {}

在类的实例上面调用方法,其实就是调用原型上的方法。

arduino 复制代码
class Point {}

var point = new Point()

console.log(point.constructor === Point.prototype.constructor) // true

更多 Class 的理解,请看阮一峰老师的 ECMAScript 6 入门

在 ES6 的 "类" 上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。

javascript 复制代码
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}
## 继承的实现

下边就列举常用的几种继承方式,搞懂这几种,应该可以混过面试了,记住: *继承的本质就是原型链*

-   原型链继承
-   借用构造函数继承
-   组合继承
-   原型式继承
-   寄生式继承
-   寄生组合式继承
-   ES6 的 Class
// 等同于下边的代码
Point.prototype.constructor = function() {}
Point.prototype.toString = function() {}
Point.prototype.toValue = function() {}

在类的实例上面调用方法,其实就是调用原型上的方法。

arduino 复制代码
class Point {}

var point = new Point()

console.log(point.constructor === Point.prototype.constructor) // true

更多 Class 的理解,请看阮一峰老师的 ECMAScript 6 入门 在类的实例上调用方法,调用原型上的方法

继承的实现

下边就列举常用的几种继承方式,搞懂这几种,应该可以混过面试了,记住: 继承的本质就是原型链

  • 原型链继承
  • 借用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6 的 Class

原型链继承

利用原型,让一个引用类型继承另一个引用类型的属性和方法;

javascript 复制代码
function Parent() {
  this.property = true
}

Parent.prototype.getValue = function() {
  return this.property
}

function Child() {
  this.childProperty = false
}

Child.prototype = new Parent() // 将父类的实例赋给子类的prototype

Child.prototype.getChildValue = function() {
  return this.childProperty
}

var ch1 = new Child()
console.log(ch1.getChildValue()) // false
console.log(ch1.getValue()) // true

继承是通过创建 Parent 的实例,并将该实例赋给 Child.prototype 实现的。实现的本质是 重写原型对象,代之以一个新类型的实例。换句换说,原来存在于 Parent 的实例中的所有属性和方法,现在也存在 Child.prototype 中了。最终结果是: ch1 指向 Child 的原型, Child 的原型指向 Parent 的原型, getValue() 方法仍在 Parent.prototype 上,而 property 位于 Child.property 中,这是因为: property 是一个实例属性,而 getValue() 是一个原型方法

ch1.constructor 现在不是指向 Child ,而是指向 Parent ,这是因为 Child .prototype 被重写的缘故。实际上,不是 Child 的原型的 constructor 属性被重写,而是 Child 的原型指向了另一个对象------Parent 的原型,而这个原型对象的 constructor 属性指向的是 Parent

ini 复制代码
// 可以这么理解
// 正常情况下
Child.prototype.constructor = Child
ch1.contructor = Child

// 但是现在 Child.prototype = new Parent() 将父类的实例赋给子类的prototype之后
Child.prototype = new Parent()(new Parent()).contructor = Parent
Child.prototype.contructor = Parent
ch1.constructor = Parent

那么原型链继承的问题有哪些呢?

原型链中的原型对象是共用的,子类无法通过父类创建私有属性, 比如你 new 两个子类 child1 和 child2 的时候,你改 child1 的属性,child2 也会跟着改变,比如下边的代码

javascript 复制代码
function Parent() {
  this.colors = ['red', 'yellow']
}

function Child() {}

// 子类继承父类
Child.prototype = new Parent()

var ch1 = new Child()
ch1.colors.push('black')
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child()
console.log(ch2.colors) // ['red', 'yellow', 'black']

你看,这就出问题了吧,因为在 Parent 构造函数中定义了一个 colors 属性,当通过原型链继承了之后,Child.prototype 就变成了 Parent 的一个实例,因此它也拥有了一个它自己的 colors 属性------就跟专门创建了一个 Child.prototype.colors 一样,那么所有 Child 的实例都会共享这个 colors 属性,而 ch1 和 ch2 都是 Child 的实例,对 ch1.colors 的修改,在 ch2.colors 中也会反映出来

借用构造函数

为了解决上边 原型链继承 存在的问题,现在使用构造函数去继承,在子类的构造函数里执行父类的构造函数, 主要通过 call / apply 去改变 this 的指向,从而导致父类构造函数执行时的这些属性都会挂载到子类实例上去

javascript 复制代码
function Parent() {
  this.colors = ['red', 'yellow']
}

function Child() {
  // 子类继承了父类
  Parent.call(this)
}

var ch1 = new Child()
ch1.colors.push('black')
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child()
console.log(ch2.colors) // ['red', 'yellow']

组合继承

将原型链和借用构造函数的技术组合在一起。背后的思路是: 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的服用,又能够保证每个实例都有它的属性

javascript 复制代码
function Parent() {
  this.name = '彭道宽' // 这叫实例属性
}

Parent.prototype.getName = function() {} // 这叫做原型属性
javascript 复制代码
function Parent(name) {
  this.name = name
  this.colors = ['red', 'yellow']
}

Parent.prototype.sayName = function() {
  console.log(this.name)
}

function Child(name, age) {
  // 借用构造函数实现继承
  Parent.call(this, name)
  this.age = age
}

// 子类通过 原型链 继承
Child.prototype = new Parent()
Child.prototype.constructor = Child // 注意, 如果没有说明,那么Child.prototype.constructor 就会是指向 Parent
Child.prototype.sayAge = function() {
  console.log(this.age)
}

var ch1 = new Child('彭道宽', 21)
ch1.colors.push('black')
ch1.sayName() // 彭道宽
ch1.sayAge() // 21
console.log(ch1.colors) // ['red', 'yellow', 'black']

var ch2 = new Child('PDK', 18)
ch1.sayName() // PDK
ch1.sayAge() // 18
console.log(ch1.colors) // ['red', 'yellow']

原型式继承

ECMAScript5 新增 Object.create()方法规范了原型式继承,这个方法接收两个参数 : 一个用作新对象原型的对象和一个为新对象定义额外属性的对象

javascript 复制代码
var Parent = {
  name: 'PDK',
  friends: ['a', 'b', 'c']
}

var ch1 = Object.create(Parent)
ch1.name = 'OB-1'
ch1.friends.push('d')

var ch2 = Object.create(Parent)
ch2.name = 'OB-2'
ch2.friends.push('e')

console.log(Parent.friends) // ['a', 'b', 'c', 'd', 'e']
console.log(Parent.name) // PDK

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

寄生式继承

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

php 复制代码
function Parent(origin) {
  var clone = Object.create(origin) // 通过调用函数来创建一个对象
  clone.sayHi = function() {
    console.log('hi')
  }
  return clone // 返回这个对象
}

var child = {
  name: 'pdk'
}
var resClone = Parent(child)
resClone.sayHi() // "hi"

寄生组合式继承

所谓的寄生组合式继承,就是通过借用构造函数来继承属性,通过原型链的混用来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后将结果指定给自类型的原型。跟组合式继承的区别在于,他不需要在一次实例中调用两次父类的构造函数。基本模式如下:

javascript 复制代码
function inheritPrototype(Child, Parent) {
  var prototype = Object.create(Parent.prototype) // 创建对象
  prototype.constructor = Child // 增强对象
  Child.prototype = prototype // 指定对象
}
javascript 复制代码
function Parent(name) {
  this.name = name
  this.colors = ['red', 'yellow']
}

Parent.prototype.sayName = function() {
  console.log(this.name)
}

function Child(name, age) {
  Parent.call(this, name)
  this.age = age
}

inheritPrototype(Child, Parent)

Child.prototype.sayAge = function() {
  console.log(this.age)
}

var ch1 = new Child('彭道宽', 21)
console.log(ch1.sayage) // 21

ES6 的 Class 继承

javascript 复制代码
class Parent {
  constructor(name) {
    this.name = name
  }

  doing() {
    console.log('parent doing something')
  }

  getName() {
    console.log('parent name: ', this.name)
  }
}

class Child extends Parent {
  constructor(name, parentName) {
    super(parentName)
    this.name = name
  }

  sayName() {
    console.log('child name: ', this.name)
  }
}

var ch1 = new Child('son', 'father')
ch1.sayName() // child name: son
ch1.getName() // parent name: son
ch1.doing() // parent doing something

var parent = new Parent('father')
parent.getName() // parent name: father

class 实现原理

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。

在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数。

scala 复制代码
class parent {}

class Child extends Parent {
  constructor() {
    super()
  }
}

注意,super 虽然代表了父类 Parent 的构造函数,但是返回的是子类 Child 的实例,即 super 内部的 this 指的是 Child,因此 super() 在这里相当于 Parent.prototype.constructor.call(this)。

scala 复制代码
class parent {}

class Child extends Parent {}

Child.__proto__ === Parent // 继承属性
Child.prototype.__proto__ === Parent.prototype // 继承方法

extends 实现原理

javascript 复制代码
//原型连接
Child.prototype = Object.create(Parent.prototype)

// B继承A的静态属性
Object.setPrototypeOf(Child, Parent)

//绑定this
Parent.call(this)

最后来两个思考题

javascript 复制代码
function SuperType() {
  this.colors = ['red', 'yellow']
}

function SubType() {}

// 继承了SuperType
SubType.prototype = new SuperType()

var instance1 = new SubType() // intance.constructor = SuperType
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'yellow', 'black']

var instance2 = new SubType()
console.log(instance2.colors) // ['red', 'yellow', 'black']

// 这里多出几道题,理解一下原型和原型链
console.log(instance1.constructor)
console.log(SubType.prototype.constructor)
console.log(SubType.prototype.__proto__ == SuperType.prototype)
console.log(instance1.__proto__ == SubType.prototype)
console.log(SubType.__proto__ == SuperType.prototype)
console.log(SubType.__proto__ == Function.prototype)
console.log(SuperType.prototype.constructor == SuperType)
console.log(SuperType.__proto__ == Function.prototype)
console.log(SuperType.prototype.__proto__ == Object.prototype)
javascript 复制代码
function SuperType() {
  this.colors = ['red', 'yellow']
}

function SubType() {
  // 继承了SuperType
  SuperType.call(this)
}

var instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'yellow', 'black']

var instance2 = new SubType()
console.log(instance2.colors) // ['red', 'yellow']

// 思考一哈?
console.log(instance1.constructor)
console.log(SubType.prototype.constructor)
console.log(SubType.prototype.__proto__)
console.log(SubType.prototype.__proto__ == SuperType.prototype)
console.log(SubType.prototype.__proto__ == Object.prototype)
console.log(instance1.__proto__ == SubType.prototype)
console.log(SubType.__proto__ == SuperType.prototype)
console.log(SubType.__proto__ == Function.prototype)
console.log(SuperType.prototype.constructor == SuperType)
console.log(SuperType.__proto__ == Function.prototype)
console.log(SuperType.prototype.__proto__ == Object.prototype)

相关链接

ES6 入门 - Class 继承: es6.ruanyifeng.com/#docs/class...

博客 : github.com/PDKSophia/b...

原型与原型链 : github.com/PDKSophia/b... 原型链继承,让一个引用类型继承外一个引用类型的属性和方法 Child.prototype就变成了Parent的一个实例, 借助构造哦函数 子类继承了父类,父类构造函数执行的时候这些属性都会被挂在到子类上去 ES6的继承机制完全不同,是指是先将父类实力对象的属性和方法加到this上面去,然后再用子类的构造函数修改this

知识模型

有良好的计算机专业基础

有扎实的编码能力且熟悉经典算法与常用数据结构
掌握基础的计算机网络知识

掌握必要的前端基础知识

  • Html

  • Css

  • Javascript

  • React

  • Vue

  • typescript

有基本的工程化能力

一切能提升前端开发效率,提高前端应用质量的手段和工具都是前端工程化。

  • webpack
  • 性能优化
  • 测试

知道常见场景的解决方案

二进制

通过 accept 限制上传文件类型

在日常工作中,文件上传是一个很常见的功能。在某些情况下,我们希望能限制文件上传的类型,比如限制只能上传 PNG 格式的图片。针对这个问题,我们会想到通过 input 元素的 accept 属性来限制上传的文件类型:

python 复制代码
<input type="file" id="inputFile" accept="image/png" />

这种方案虽然可以满足大多数场景,但如果用户把 JPEG 格式的图片后缀名更改为 .png 的话,就可以成功突破这个限制。因为通过 文件后缀名或文件的 MIME 类型 并不能识别出正确的文件类型。我们可以通过读取文件的二进制数据来识别正确的文件类型。

如何通过二进制查看图片的类型

计算机并不是通过图片的后缀名来区分不同的图片类型,而是通过 Magic Number 来区分。 对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。

常见图片类型对应的 Magic Number 如下表所示:

文件类型 文件后缀 魔数
JPEG jpg/jpeg 0xFF D8 FF
PNG png 0x89 50 4E 47 0D 0A 1A 0A
GIF gif 0x47 49 46 38(GIF8)
BMP bmp 0x42 4D

readBuffer 函数

在获取文件对象后,我们可以通过 FileReader API 来读取文件的内容。因为我们并不需要读取文件的完整信息,所以阿宝哥封装了一个 readBuffer 函数,用于读取文件中指定范围的二进制数据。

ini 复制代码
function readBuffer(file, start = 0, end = 2) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.onerror = reject;
    reader.readAsArrayBuffer(file.slice(start, end));
  });
}

检测 PNG 图片类型

对于 PNG 类型的图片来说,该文件的前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A 。因此,我们在检测已选择的文件是否为 PNG 类型的图片时,只需要读取前 8 个字节的数据,并逐一判断每个字节的内容是否一致。基于前面定义的 readBuffercheck 函数,我们就可以实现检测 PNG 图片的功能:

代码

HTML 代码

bash 复制代码
<div>
   选择文件:<input type="file" id="inputFile" accept="image/*" onchange="handleChange(event)" />
   <p id="realFileType"></p>
</div>

JS 代码

ini 复制代码
// 逐字节比对工具函数
function check(headers) {
  return (buffers, options = { offset: 0 }) =>
    headers.every(
      (header, index) => header === buffers[options.offset + index]
    );
}
// 检测 PNG 函数
const isPNG = check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); 


const realFileElement = document.querySelector("#realFileType");

async function handleChange(event) {
  const file = event.target.files[0];
  const buffers = await readBuffer(file, 0, 8);
  const uint8Array = new Uint8Array(buffers);
  realFileElement.innerText = `${file.name}文件的类型是:${
    isPNG(uint8Array) ? "image/png" : file.type
  }`;
}

检测 JEPG 类型 isJPEG 函数:

ini 复制代码
const isJPEG = check([0xff, 0xd8, 0xff])

检测 PDF 类型

PDF 文件的头 4 个字节的是 0x25 50 44 46 ,对应的字符串是 %PDF 。为了让用户能更直观地辨别出检测的类型,可定义了一个 stringToBytes 函数:

typescript 复制代码
function stringToBytes(string) {
  return [...string].map((character) => character.charCodeAt(0));
}

基于 stringToBytes 函数,我们就可以很容易的定义一个 isPDF 函数,具体如下所示:

ini 复制代码
const isPDF = check(stringToBytes("%PDF"));

计算机通过magic number来区分的,

优雅代码

If 语句

bad

ini 复制代码
if (value === "duck" || value === "dog" || value === "cat") {
  // ...
}

good

ini 复制代码
const options = ["duck", "dog", "cat"];
if (options.includes(value)) {
  // ...
}

提前退出机制

csharp 复制代码
function handleEvent(event) {
  if (event) {
    const target = event.target;
    if (target) {
      // ...
    }
  }
}

使用提前退出,减少嵌套

csharp 复制代码
function handleEvent(event) {
  if (!event || !event.target) {
    return;
  }
  // ...
}

返回数组

如果一个函数只有少量几个返回值,但调用方在使用此函数时大概率会将返回的值改名,那就返回数组

bad

ini 复制代码
const useState = () => {
  return {
    state,
    setState,
  };
};

good

ini 复制代码
// 函数
const useState = () => {
  return [state, setState];
};

// 调用
const [data, setData] = useState();

使用 Map 来代替静态的 switch case

如果明确知道映射关系,如 TS 类型定义好了。并且没有什么计算逻辑,就可以使 Map 提示代码整洁度。

bad

javascript 复制代码
const getTranslate = (en) => {
  switch (en) {
    case "apple":
      return "苹果";
    case "banana":
      return "香蕉";
    case "orange":
      return "橘子";
  }
};

good

ini 复制代码
const translateMap = {
  apple: "苹果",
  banana: "香蕉",
  orange: "橘子",
};
const getTranslate = (en) => {
  return translateMap[en];
};

package-lock.json

概括很简单,就是锁定安装时的包的版本号,并且需要上传到 git,以保证其他人在 npm install 时大家的依赖能保证一致。

根据官方文档,这个 package-lock.json 是在 npm install时候生成一份文件,用以记录当前状态下实际安装的各个 npm package 的具体来源和版本号。

它有什么用呢?因为 npm 是一个用于管理 package 之间依赖关系的管理器,它允许开发者在 pacakge.json 中间标出自己项目对 npm 各库包的依赖。你可以选择以如下方式来标明自己所需要库包的版本

这里举个例子:

perl 复制代码
"dependencies": {
    "@types/node": "^8.0.33",
},

这里面的 向上标号^是定义了向后(新)兼容依赖,指如果 types/node 的版本是超过 8.0.33,并在大版本号(8)上相同,就允许下载最新版本的 types/node 库包,例如实际上可能运行 npm install 时候下载的具体版本是 8.0.35。

大多数情况这种向新兼容依赖下载最新库包的时候都没有问题,可是因为 npm 是开源世界,各库包的版本语义可能并不相同,有的库包开发者并不遵守严格这一原则:相同大版本号的同一个库包,其接口符合兼容要求。这时候用户就很头疼了:在完全相同的一个 nodejs 的代码库,在不同时间或者不同 npm 下载源之下,下到的各依赖库包版本可能有所不同,因此其依赖库包行为特征也不同有时候甚至完全不兼容。

因此 npm 最新的版本就开始提供自动生成 package-lock.json 功能,为的是让开发者知道只要你保存了源文件,到一个新的机器上、或者新的下载源,只要按照这个 package-lock.json 所标示的具体版本下载依赖库包,就能确保所有库包与你上次安装的完全一样。

package-lock.json | 官方文档

semver 语义化版本号变更

^ 是 npm 默认的版本符号, 当你使用 npm install --save 时, npm 会自动在 package 中添加^加上版本号. 例如: npm install --save angular 会在 package.json 中添加"angular": "^1.3.15".这个符号会告诉 npm 可以安装 1.3.15 或者一个大于它的版本, 但是要是主版本 1 下的版本.

~ 同样被用来做 npm 得版本控制, 例如1.3.15, 代表了 npm 可以安装 1.3.15 或者更高的版本, 与^的区别在于, 的版本只能开始于次版本号 1.3. 它们的作用域不同. 你可以通过 npm config set save-prefix=' '将设置为默认符号.

>符号主要是用来指定可以安装 beta 版本.

semver 版本号 | 官方文档

devDependencies 节点下的模块是我们在开发时需要用的,比如项目中使用的 gulp ,压缩 css、js 的模块。这些模块在我们的项目部署后是不需要的,所以我们可以使用 -save-dev 的形式安装 devDependncies节点下的模块是开发的时候需要用的, npm最新的版本就开始提供自动生成的package-lock.json功能,

Git

Git 快照

Every time you commit, or save the state of your project in Git, it basically takes a picture of what all your files look like at that moment and stores a reference to that snapshot.

Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个快照流。

In computer systems, a snapshot is the state of a system at a particular point in time. git当中保存项目状态,主要对当时的全部文件制作一个快照并且保存这个快照的索引,

三种状态

  • 已提交 committed
  • 已暂存 staged
  • 已修改 modified

三个区域

  • Git 仓库

    • 是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。
  • 暂存区域

    • 暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作`'索引'',不过一般说法还是叫暂存区域。
  • 工作目录

    • 工作目录是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。 已提交:;committed,已经暂存:他哥的,,已经修改:modified

基本的 Git 工作流程

  1. 在工作目录修改文件
  2. 暂存文件,将文件的快照放入暂存区
  3. 提交更新,找到暂存去文件,将快照永久性存储到 Git 仓库目录。

用户信息

当安装完 Git 应该做的第一件事就是设置你的用户名称与邮件地址。 这样做很重要,因为每一个 Git 的提交都会使用这些信息,并且它会写入到你的每一次提交中,不可更改。

lua 复制代码
git config --global user.name "huyaocode"
git config --global user.email johndoe@example.com

加入暂存区

csharp 复制代码
git add 文件名或路径

忽略文件

创建一个.gitignore文件,可描述需要忽略的文件。 参考

yaml 复制代码
# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# 只忽略当前文件夹下已 TODO 为名的文件
/TODO
# 忽略当前目录下 build 这个文件夹
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf

状态修改

git status -s将每个修改状态以一行展示,也可以用git status多行展示。

  • A 新添加到暂存区中的文件
  • M 修改过的文件
  • D 被删除的文件
  • MM 出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。
  • ?? 未跟踪

查看修改

  • 要查看尚未暂存的文件更新了哪些部分,不加参数直接输入 git diff
  • 要查看已暂存的将要添加到下次提交里的内容,可以用 git diff --cachedgit diff --staged,这两个命令结果是一样的。
  • git diff HEAD: 对比工作区(未 git add)和版本库(git commit 之后)

提交修改

运行git commit,会出现如下情况。这种方式会启动文本编辑器,开头还有一空行,供你输入提交说明。下面的行是被注释了的,也可以取消这些注释。

一般是 vim 或 emacs。当然也可以按照 起步 介绍的方式,使用 git config --global core.editor 命令设定你喜欢的编辑软件。

也可以使用git commit -m "修改描述" 这种直接输入描述的方式提交修改。

git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤

移除文件

要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。 可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

运行 git rm记录此次移除文件的操作。下一次提交时,该文件就不再纳入版本管理了。 如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删还没有添加到快照的数据, 这样的数据不能被 Git 恢复。

想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。(不想让 Git 跟踪)

bash 复制代码
git rm --cached 某文件

文件更名

bash 复制代码
git mv file_from file_to

其实,运行 git mv 就相当于运行了下面三条命令:

bash 复制代码
mv README.md README
git rm README.md
git add README

查看提交历史

git loggit log 会按提交时间列出所有的更新,最近的更新排在最上面。 正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

使用 -p 用来限制展示条数。git log -p -2

使用 --stat 选项看到每次提

使用format,定制要显示的记录格式。

使用--graph可形象地展示你的分支、合并历史。

sql 复制代码
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
* 11d191e Merge branch 'defunkt' into local

重新提交

有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend 选项的提交命令尝试重新提交。

sql 复制代码
git commit --amend

这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令),那么快照会保持不变,而你所修改的只是提交信息。

文本编辑器启动后,可以看到之前的提交信息。 编辑后保存会覆盖原来的提交信息。

例如,你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作:

sql 复制代码
git commit -m 'initial commit'
git add forgotten_file
git commit --amend

最终你只会有一个提交 - 第二次提交将代替第一次提交的结果。

取消暂存的文件

使用 git reset HEAD <file> 来取消暂存。在调用时加上 --hard 选项可以令 git reset 成为一个危险的命令(译注:可能导致工作目录中所有当前进度丢失!)

撤消对文件的修改

使用git checkout -- <file> 可以撤销修改(未保存到暂存区)

远程仓库

  • 添加远程仓库

    • git remote add <shortname> <url>
  • 从远程仓库中抓取与拉取

    • git fetch [remote-name]
  • 推送到远程仓库

    • git push [remote-name] [branch-name]
  • 查看远程仓库

    • git remote show [remote-name]
  • 远程仓库的重命名

    • git remote rename

打标签

Git 可以给历史中的某一个提交打上标签,以示重要。

Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。通常建议创建附注标签。

  • 一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。
  • 附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证。

列出标签git tag

附注标签

  • 创建

    • git tag -a v1.4 -m '描述'
  • 查看某版本

    • git show 版本号

轻量标签

  • 轻量标签本质上是将提交校验和存储到一个文件中 - 没有保存任何其他信息,不些描述
  • git tag v1.4-lw

共享标签

  • 默认情况下,git push 命令并不会传送标签到远程仓库服务器上。创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样 - 你可以运行 git push origin [tagname]
  • 如果想要一次性推送很多标签,也可以使用带有 --tags 选项的 git push 命令。 这将会把所有不在远程仓库服务器上的标签全部传送到那里。

删除标签

  • 删本地,并不会从任何远程仓库中移除这个标签

    • git tag -d <tagname>
  • 删远程

    • git push <remote> :refs/tags/<tagname>

Git 别名

可为一些操作器别名,例如: git config --global alias.last 'log -1 HEAD'后, 使用git last 就可以看到最后一次提交

git conifg --global alias.last 'log -1 HEAD' 辅助标签是存储在git数据库当中一个完整对象,-p用来限制展示的条数,--stat选项看到每次提交format -amend提交命令常识重新提交 git reset HEAD file git checkout --file撤销修改米有保存到暂存区 git chckout --file可以撤销修改没有保存到暂存区 git remote rename git remoate how [remotee-name] git push [remoet-name][branch-name] git fetch [remote-name

Formik

Formik 是 React 中构建表单时所用的库,它可以做以下 3 件事:

  1. 在 Form 的内外都可获取 Form 的 State
  2. 在验证表单时管理错误信息
  3. 处理 Form 的提交操作

主要组件有如下:

  • <Formik />

    • 构建整个表单的顶层组件,通过自身的 state 维护表单状态(2.0 版本采用 useReducer 维护),收集表单字段,执行校验和提交表单等操作,通过 render 函数或者 children 的方式往下传递 props,使得子组件具备完全访问和控制表单状态的能力;
  • <Form />

    • 是对<form />标签的简单封装,监听了 form 的 onReset 和 onSubmit 事件;
  • <Field />

    • 通过指定 name 属性与 formik 表单状态中具体某个字段建立「同步关系」,这个组件帮我们省去了很多对受控组件的更新操作,如 onChange、onBlur 事件的监听和回调处理。<Field />组件默认渲染为一个 input,同时也可以通过 render 属性或者 children 的方式渲染自己的字段组件,如 select,checkbox,textarea 等。
  • <FieldArray />

    • 这个组件可以对一个数组字段进行维护,通过其提供的 props 方法中的 insert 或者 remove 可以方便的增删表单中的某个数组字段里的元素,在进行动态控制表单字段方面很方便,具体示例可以参考后面的多语言输入框的实现。 field制定name属性和formik表单状态当中具体某个字段建立同步关系,这个组件帮我们省去了很多对受控组件的更新操作,

TabNine

一个机器学习驱动的代码自动补全工具 TabNine:一个机器学习驱动的代码自动不全工具

decorateComponentWithProps

一个简单的工具函数包装你的组件,让它得到额外的参数。

github: github.com/belle-ui/de...

源码

scala 复制代码
const decorateComponentWithProps = (EmbeddedComponent, props) => (class extends Component {
  render() {
    return (
      <EmbeddedComponent { ...this.props } { ...props } />
    );
  }
});

用法

ini 复制代码
const props = {
  wine: 'red',
  beer: 'ipa',
  food: 'spaghetti',
};

MyDecoratedComponent = decorateComponentWithProps(MyComponent, props);

函数式编程

juejin.im/post/684490... juejin.im/post/684490...

/**

  • 广度优先遍历树
  • 使用: 队列 */ function widthTravel(tree) { let queue = []; let nodeList = []; tree && queue.push(tree); while (queue.length) { let node = queue.shift(); for (let i = 0; i < node.children.length; i++) { nodeList.push(node[i]); } } return nodeList; }

/**

  • 递归式深度遍历tree */ function deepTravel(tree, nodeList = []) { if (tree) { nodeList.push(tree); for (let i of Object.keys(tree.children)) { deepTravel(tree.children[i], nodeList); } } return nodeList; }

/**

  • 非递归式 遍历tree
  • 使用:栈
  • 但入栈时是反着把 children 数组 push 入栈的,保证下一次 pop 能拿到左子树元素 */ function deepTravel(tree) { let stack = []; let nodeList = []; tree && stack.push(tree); //注意,这里如果 while(stack) 会死循环 while (stack.length) { let node = stack.pop(); nodeList.push(node); for (let i = node.children.length - 1; i >= 0; i--) { stack.push(node.children[i]); } } }

冒泡排序

每次两两比较,大的放到后面,第 i 轮找出 第 n - i 轮大的数

ini 复制代码
function bubbleSort(arr) {
  // 外层,需要遍历的次数
  for (let i = 1; i < arr.length; i++) {
    // 内层,每次比较
    for (let j = 0; j < arr.length - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
}

选择排序

每一轮从数组的未排序部分加一开始,找到一个最小的值的索引,然后与未排序将其放到未排序部分的最左位置。

ini 复制代码
function selectionSort(arr) {
  // 选多少次
  for (let i = 0; i < arr.length - 1; i++) {
    let minIndex = i;
    // 在arr[i + 1, ] 中找最小值索引, i+1 代表有序的下一个数,我们默认第一个元素是最小的
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex != i) {
      // 交换
      let temp = arr[minIndex];
      arr[minIndex] = arr[i];
      arr[i] = temp;
    }
  }
}

插入排序

为当前元素保存一个副本,依次向前遍历前面的元素是否比自己大,如果比自己大就直接把前一个元素赋值到当前元素的位置,当前某位置的元素不再比当前元素大的时候,将当前元素的值赋值到这个位置。

ini 复制代码
function insertSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    let j,
      temp = arr[i];
    for (j = i; j > 0 && arr[j - 1] > temp; j--) {
      arr[j] = arr[j - 1];
    }
    arr[j] = temp;
  }
}

快排

ini 复制代码
/**
 * 将数组arr分为两部分,前一部分整体小于后一部分
 */
function partition(arr, left, right) {
  // 交换数组最左元素与数组的中间元素
  let midIndex = ((left + right) / 2) >> 0;
  swap(arr, left, midIndex);
  // 基准元素
  const flagItem = arr[left];
  let i = left + 1,
    j = right;
  while (true) {
    while (i <= right && arr[i] < flagItem) {
      i++;
    }
    while (j >= left && arr[j] > flagItem) {
      j--;
    }
    if (i > j) {
      break;
    } else {
      swap(arr, i, j);
      i++;
      j--;
    }
  }
  swap(arr, left, j);
  return j;
}

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left >= right) {
    return;
  }
  const mid = partition(arr, left, right);
  quickSort(arr, left, mid - 1);
  quickSort(arr, mid + 1, right);
}

归并排序

ini 复制代码
/**
 * 归并数组的两个有序部分
 *
 * 将arr[left, mid], arr[mid, right]两部分归并
 */
template <typename T>
void __merge(T arr[], int left,int mid, int right){

  T tempArr[right - left + 1]; //创建临时空间
  for (int i = left; i <= right; i++) {
    tempArr[i - left] = arr[i];
  }

  // i,j分别为数组两部分的游标
  int i = left, j = mid + 1;
  for(int k = left; k <= right; k++) {

    //考虑越界的情况
    if( i > mid ) {
      arr[k] = tempArr[j - left];
      j ++;
    }
    else if(j > right) {
      arr[k] = tempArr[i - left];
      i ++;
    }
    //不越界
    else if(tempArr[i - left] < tempArr[j - left]) {
      arr[k] = tempArr[i - left];
      i ++;
    } else {
      arr[k] = tempArr[j - left];
      j ++;
    }
  }
}


/**
 * 递归使用归并排序,队arr[left, right]范围进行排序
 */
template <typename T>
void __mergeSort(T arr[], int left, int right){

  if (left >= right) {
    return;
  }

  int mid = (left + right) / 2;
  __mergeSort(arr, left, mid);
  __mergeSort(arr, mid + 1, right);
  //将两个数组进行归并
  if(arr[mid] > arr[mid+1]) { //这个判断可以很大程度提升再接近有序时的性能
    __merge(arr, left, mid, right);
  }
}

堆排序

ini 复制代码
/**
 * shiftDown 沿着树不断调整位子
 * 比较左右子树,是否有比自己大的,如果有就和大的那个交换位置
 * 使得大的再上,小的在下
 * 为了维持堆的特性,还得把那个交换到子树上的较小元素拿去尝试对他进行shiftDown
 */
template <typename T>
void __shiftDown(T arr, int pos, int len) {
  while(2 * pos + 1 < len){
    int j = pos * 2 + 1;  //默认左孩子
    if(j + 1 < len) { //如果有右孩子
      if(arr[j + 1] > arr[j]) {
        j += 1;
      }
    }
    if(arr[pos] < arr[j]) {
      swap(arr[pos], arr[j]);
      pos = j;  //--这步容易忘,因为是循环,所以每次都要对pos进行shiftDown--
    } else {
      break;
    }
  }
}
/**## 实现一个 sleep 函数

比如 sleep(1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await 等角度实现

### [](https://github.com/huyaocode/webKnowledge/blob/master/3-%E7%BC%96%E7%A8%8B%E8%83%BD%E5%8A%9B/%E7%BC%96%E7%A8%8B%E9%A2%98%E4%B8%8E%E5%88%86%E6%9E%90%E9%A2%98/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAsleep%E5%87%BD%E6%95%B0.md#promise)Promise

const sleep = (time) => { new Promise((resolve) => { setTimeout(resolve, time); }); };

sleep(1000).then(() => { console.log(1); });

less 复制代码
### [](https://github.com/huyaocode/webKnowledge/blob/master/3-%E7%BC%96%E7%A8%8B%E8%83%BD%E5%8A%9B/%E7%BC%96%E7%A8%8B%E9%A2%98%E4%B8%8E%E5%88%86%E6%9E%90%E9%A2%98/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAsleep%E5%87%BD%E6%95%B0.md#generator)Generator

function* sleep(time) { yield new Promise((resolve) => { setTimeout(resolve, time); }); } sleep(1000) .next() .value.then(() => { console.log(1); });

less 复制代码
### [](https://github.com/huyaocode/webKnowledge/blob/master/3-%E7%BC%96%E7%A8%8B%E8%83%BD%E5%8A%9B/%E7%BC%96%E7%A8%8B%E9%A2%98%E4%B8%8E%E5%88%86%E6%9E%90%E9%A2%98/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAsleep%E5%87%BD%E6%95%B0.md#async)async

async function sleep(time, func) { await new Promise((resolve) => setTimeout(resolve, time)); return func(); } sleep(1000, () => { console.log(1); });

less 复制代码
### [](https://github.com/huyaocode/webKnowledge/blob/master/3-%E7%BC%96%E7%A8%8B%E8%83%BD%E5%8A%9B/%E7%BC%96%E7%A8%8B%E9%A2%98%E4%B8%8E%E5%88%86%E6%9E%90%E9%A2%98/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAsleep%E5%87%BD%E6%95%B0.md#es5)ES5

function sleep(callback, time) { if (typeof callback === "function") setTimeout(callback, time); }

function output() { console.log(1); } sleep(output, 1000);

scss 复制代码
 * 通用堆排序
 */
template <typename T>
void heapSort(T arr[], int n)
{
  //Heapify
  for(int i = (n-1)/2; i >= 0; i-- ) {
    __shiftDown(arr, i, n);
  }
  //出堆,放到数组末尾
  for(int i = n - 1; i > 0; i--){
    swap(arr[i], arr[0]);
    __shiftDown(arr, 0, i);
  }
}

选择排序:每一轮从数组的某排序部分加一开始,找到一个最小值的索引,然后和没有排序的将其放到未排序的部分

实现一个 sleep 函数

比如 sleep(1000) 意味着等待 1000 毫秒,可从 Promise、Generator、Async/Await 等角度实现

Promise

javascript 复制代码
const sleep = (time) => {
  new Promise((resolve) => {
    setTimeout(resolve, time);
  });
};

sleep(1000).then(() => {
  console.log(1);
});

Generator

javascript 复制代码
function* sleep(time) {
  yield new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}
sleep(1000)
  .next()
  .value.then(() => {
    console.log(1);
  });

async

javascript 复制代码
async function sleep(time, func) {
  await new Promise((resolve) => setTimeout(resolve, time));
  return func();
}
sleep(1000, () => {
  console.log(1);
});

ES5

scss 复制代码
function sleep(callback, time) {
  if (typeof callback === "function") setTimeout(callback, time);
}

function output() {
  console.log(1);
}
sleep(output, 1000);
相关推荐
中微子1 小时前
🔥 React Context 面试必考!从源码到实战的完整攻略 | 99%的人都不知道的性能陷阱
前端·react.js
中微子2 小时前
React 状态管理 源码深度解析
前端·react.js
加减法原则3 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele3 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4533 小时前
React移动端开发项目优化
前端·react.js·前端框架
你的人类朋友4 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir4 小时前
vue3.2 前端动态分页算法
前端·算法
烛阴4 小时前
WebSocket实时通信入门到实践
前端·javascript
草巾冒小子4 小时前
vue3实战:.ts文件中的interface定义与抛出、其他文件的调用方式
前端·javascript·vue.js
DoraBigHead5 小时前
你写前端按钮,他们扛服务器压力:搞懂后端那些“黑话”!
前端·javascript·架构