第二十二章 高级技巧

1. 类型检测的方式有哪些, 使用场景是?

  • 使用 typeof 检测基础类型
  • 使用 instanceof 检测引用类型
  • 使用 Object.prototype.toString.call(value)可以处理大部分情况

2. 如果类没有用new关键字初始化一般会造成什么后果, 怎么避免?

在构造函数中的属性会污染全局变量window, 可以在构造函数中通过instanceof判断下当前实例的构造函数是不是自己, 如果不是则使用new方法返回.

function Person(name, age, job) {
  if (this instanceof Person) {
    this.name = name;
    this.age = age;
    this.job = job;
  } else {
    return new Person(name, age, job);
  }
}

3. 补充一个完整类继承及实例化的示例?

// 1. 父类定义
function SuperType(name) {
  if (this instanceof SuperType) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
  } else {
    return new SuperType(name);
  }
}

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

// 2. 子类定义
function SubType(name, age) {
  if (this instanceof SubType) {
    // 2.1 继承了 SuperType 构造函数中定义的属性
    SuperType.call(this, name);
    // 2.2 子类自己的属性
    this.age = age;
  } else {
    return new SubType(name, age);
  }
}

// 父类原型链复制
SubType.prototype = Object.create(SuperType.prototype);
// 因为使用“.prototype =...”后,constructor会改变为“=...”的那个
// constructor,所以要重新指定.constructor 为自身。
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

补充 Object.create 的 polyfill:

(function () {
    if(typeof Object.prototype.create === 'undefined') {
        Object.prototype.create = function (proto) {
            function F () {}
            F.prototype = proto
            return new F()
        }
    }
})()
}

4. 讲一个惰性函数的实例场景, 优缺点?

  • 优点: 避免不必要分支执行
  • 缺点: 运行第一次牺牲点性能
  • 适用于初次运行就能确定分支方向的场景, 比如能力判断/平台判断等

两种写法:

function doSomething() {
  if (xx1) {
    doSomething = function() {};
  } else {
    doSomething = function() {};
  }
  return doSomething();
}
var doSomething = (function() {
  if (xx1) {
    return function() {};
  } else {
    return function() {};
  }
})();

5. bind()函数使用场景及优缺点?

支持原生bind()函数的浏览器有: IE9+/Firefox4+/Chrome 等, 其他浏览器需要 Polyfill:

(function() {
  if (typeof Function.prototype.bind === "undefined") {
    Function.prototype.bind = function(obj) {
      const _self = this;
      return function() {
        return _self.apply(obj, arguments);
      };
    };
  }
})();
  • bind()函数固定了执行环境, 对于异步环境保持this指向有很大帮助
  • 类似于: setTimeout/setInterval/事件绑定等
  • 与普通函数相比会使用更多开销和内存, 使用时注意

6. 如何理解函数柯里化?

函数柯里化:用于创建一个已经设置好一个或多个参数的函数。简单的说,对函数的一次分装,简化操作的方式。这个过程需要创建一个闭包,指定某个入参后返回新函数。

这个过程可能会用到:callapplyArray.prototype.slice.call(arguments, n)bind等函数。

7. 在编写库时, 如何防止核心对象被意外修改?

根据对象防篡改等级, 可以分为以下几种:

第一级: 不可扩展对象Object.preventExtensions(obj)

  • 不可再添加属性和方法
  • 已有属性和方法不受影响
  • 通过Object.isExtensible(obj)判断是否可拓展
var person = { name: "Nicholas" };
Object.preventExtensions(person);

person.age = 29;
alert(person.age); //undefined

第二级: 密封对象Object.seal(obj)

  • 继承上一级
  • 已有成员不可删除, 但属性值可修改
  • 通过Object.isSealed(obj)判断是否被密封
var person = { name: "Nicholas" };
Object.seal(person);

person.age = 29;
alert(person.age); //undefined

delete person.name;
alert(person.name); //"Nicholas"

第三级: 冻结对象Object.freeze(obj)

  • 继承上两级
  • 以后成员不可修改
  • 如果要修改需要重新定义[[set]]函数
  • 通过Object.isFrozen(obj)判断是否被冻结

函数设计的意义

对 JavaScript 库的作者而言,冻结对象是很有用的。因为 JavaScript 库最怕有人意外(或有意)地修 改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发生。

8. JavaScript 的定时器相关点

  • JavaScript 运行在单线程环境中, 某个时刻只有一个代码在执行.
  • 异步代码会被推入独立的异步队列(Tasks)
  • 在 JavaScript 中代码不会立刻执行的,但一旦进程空闲则尽快执行
  • 浏览器在这个过程中只负责排序调度, 指派某段代码在某个时间点运行的优先级.
  • 定时器队列是一个特殊的队列, 当定时的时间到了, 就将代码插入异步队列
  • 定时器中设定的时间表示何时将定时器的代码添加到异步队列, 而不是何时执行代码
  • 队列中的所有代码都要等到 JavaScript 进程空闲后才能执行, 而不管他们是如何添加到队列中
  • 即使执行完一个异步代码(Tasks), 也会有一个很短的时间间隔, 用于处理页面上的其他事情(UI 更新), 防止页面锁定

9. 每个 Tasks 异步都是紧挨着立即执行吗?

不是, 中间会有段时间间隔以便 UI 等工作,当 JavaScript 主进程空闲时, 才将 Tasks 中的异步代码插入 stack 中。

10. 当setInterval中的函数处理时间超过设置的间隔会发生什么问题, 怎么避免或修复?

注意点:

当在 Tasks 队列中没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保定时器代码被加入到 Tasks 队列中最短时间间隔为指定的间隔。这里是加入到 Tasks 队列,不是立即执行。

因此:

  • 某些定时器回调会被跳过
  • 多个定时器代码执行之间的间隔会比预期小

可以使用两个setTimeout解决:

setTimeout(function() {
  //处理中
  setTimeout(arguments.callee, interval);
}, interval);

11. 如果一个 JS 函数执行时间接近 1s, 如何优化执行不会让 UI 卡顿?

  • 不必要的同步部分放入 setTimeout 中
  • 使用问题 10 中的方式处理,间隔时间设为 0

12. 函数防抖是如何控制调用执行的次数的?

clearTimeoutsetTimeout的组合使用

13. debounce 和 throttle 两个函数的区别?

debounce

防抖函数,多次触发只执行最后一次,保证 ideal 时间。比如 input 联想、window 的 resize 事件。

function debounce(fn, delay, context) {
  fn.timer && clearTimeout(fn.timer);
  fn.timer = setTimeout(function() {
    fn.call(context);
    clearTimeout(fn.timer);
    fn.timer = null;
  }, delay);
  return {
    clear: function() {
      clearTimeout(fn.timer);
      fn.timer = null;
    }
  };
}

需要注意的地方:

  • 计时器 timer 放在要处理的函数上, 因为是引用关系, 下次进入能携带 timer 信息
  • 每次执行完毕记得清理 timer 信息, 防止下次调用产生问题
  • clear 主要作用是清理定时器

throttle

节流函数,根据时间节奏触发,保证间隔。比如点击事件保证间隔,scroll 时控制在 16.6ms 内执行一次等。

function throttle(fn, delay, context) {
  if (fn.timer) return;
  fn.timer = setTimeout(function() {
    clearTimeout(fn.timer);
    fn.timer = null;
  }, delay);
  fn.call(context);
  return {
    clear: function() {
      clearTimeout(fn.timer);
      fn.timer = null;
    }
  };
}

需要注意的地方:

  • 计时器 timer 放在要处理的函数上, 因为是引用关系, 下次进入能携带 timer 信息
  • 每次执行完毕记得清理 timer 信息, 防止下次调用产生问题
  • 先设置定时, 再去 call 函数

14. 实现一个自定义事件库需要用到哪个设计模式?

观察者模式

15. 设计自定义事件库必须具备的功能有哪些?

  • 定义一个 hash 对象,key 为事件名,value 为回调函数数组
  • 事件包括的方法有:on、off、once、emit
  • 参考这个库的设计: events

16. 简述拖放实现的方式是?

监听 mousedown、mouseup、mousemove 事件然后进行相应处理。

  • mousedown:记录拖动元素
  • mousemove:对拖动元素修改 style 的 top、left 属性
  • mouseup:清理操作
Show Comments