红皮书里的细节

理念: 尽信书则不如无书

重载

Java/C++ 中是有函数重载的(即相同函数名, 但传的参数不同是当成不同函数的), 而 JavaScript 没有函数重载, 旧其本质是因为 JavaScrit 中函数是一个对象。函数名类似一个指针。

label 语句的用法

区分以下两段函数:

let num = 0
for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i === 5 && j === 5) {
      break
    }
    num++
  }
}
console.log(num) // 95

使用 label 语句:

let num = 0
outPoint:
for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (i === 5 && j === 5) {
      break outPoint
    }
    num++
  }
}
console.log(num) // 55

函数声明和函数表达式

  • function 开头的是函数声明, 不是以 function 开头的则是函数表达式
  • 函数声明具有函数声明提升的概念(function decleration hoisting)
// 函数声明
function test() {}

// 函数表达式
const test = function() {}

// 函数表达式
(function(){})()

函数声明提升

var a = function() {
  test()
  function test() {
    console.log('函数声明提升')
  }
}

a()

属性类型

大体针对 Object.defineProperty()Object.defineProperties 这两个 api 来讲的。使用这两个 api 创建的对象里的数据类型和访问器类型默认为 false(Configuble、Enummerable、Writable) 以及 undefined(Value、Set、Get)。

注意: 在对象上直接定义的属性, Configurable、Enumerable、Writable 默认为 true

数据类型

  • Configurable: 是否能 delete
  • Enumerable
  • Writable
  • Value

访问器类型

  • Configurable
  • Enumerable
  • Writable
  • Get
  • Set

创建对象的方式

这部分知识点和继承相通, 可联系起来;

工厂模式

function createPeople(name, age) {
  const obj = new Object()
  obj.name = name
  obj.age = age
  return obj
}

createPeople('Jack', 10)

缺点: 不知道创造的对象属于什么类

构造函数模式

function People(name, age) {
  this.name = name
  this.age = age
  this.sayHi = function() { console.log('hi') }
}

const people1 = new People('Jack', 10)
const people2 = new People('Lucy', 8)

people1.sayHi === people2.sayHi // false

优点: 能将自定义参数传入构造函数 缺点: 没有解决公共方法的复用性(是缺点也是优点, 后面有用到这个特性)

原型链模式

function People(name, age) {
  this.name = name
  this.age = age
}

People.prototype.sayHi = function() { console.log('hi') }
People.prototype.habbit = ['reading']

const people1 = new People('Jack', 10)
const people2 = new People('Lucy', 8)

原型模式实际上必须结合构造函数一起使用, 但在这里为了说明原型模式的缺点, 单独列了出来。

// 结果
people1.sayHi === people2.sayHi // true

people1.habbit.push('drawing')
people2.habbit // ['reading', drawing]

优点: 解决公共方法的复用性(sayHi); 缺点: 也正是复用性, 所以在一个实例上修改 prototype 上的属性会对其它实例也产生相同影响(habbit);

构造函数模式 + 原型链模式

这个模式也是目前被大家最为认可的一种方式, 对上述例子稍作修改:

function People(name, age) {
  this.name = name
  this.age = age
  this.habbit = ['reading']
}

People.prototype.sayHi = function() { console.log('hi') }

const people1 = new People('Jack', 10)
const people2 = new People('Lucy', 8)

// 结果
people1.sayHi === people2.sayHi // true

people1.habbit.push('drawing')
people2.habbit // ["reading"]

现在能直观地看到, people1 和 people2 公用同一个 sayHi 方法, 但是其它的属性 name、age、habbit 都是各自独立拥有的。

结论: 所谓的构造函数模式 + 原型链模式即公有方法使用原型链模式, 私有方法使用构造函数模式;从而发挥各自的优点。

继承

继承优化

const/let 出现的原因

提到块级作用域可以联系到 'const/let 出现的原因' 或者 'var 的缺点'

// 案例 1
var a = 1
var a

a // 1

------------

// 案例 2
var a = 1
var a = 2

a // 2

可以看到使用 var 并不会告知之前是否已经声明过该变量, 案例 1 直接无视了后续的声明, 案例 2 后续的声明覆盖了前面的声明, 这样子使用起来便有些混乱了。这也是 const/let 出现的原因, const 专注案例 1 的情形, let 则专注案例 2 的情形。

// 案例 1
const a = 1
const a

// Uncaught SyntaxError: Missing initializer in const declaration

------------

// 案例 2
let a = 1
let a = 2

a // 2

块级作用域

关键字: 匿名函数

(function() {
  // 块级作用域
})()

因为没有引用指向匿名函数, 所以执行完就可以垃圾回收, 不造成内存浪费。

作用域安全的构造函数

function Safe(value) {
  if (this instanceof Safe) {
    this.name = value
  } else {
    return new Safe(value)
  }
}

这种写法不管使用 new Safe() 还是 Safe() 能保证它们返回结果一致(作用域一致)。

防篡改对象

  • Object.preventExtensions(obj): obj 不能添加属性
  • Object.seal(obj): obj 不能添加/删除属性
  • Object.freeze(obj): obj 不能添加/删除/修改属性

防抖和节流函数最简版

防抖: 多次触发事件只执行一次(适用于断续的事件, 比如 clickinput)

function debounce(fn, time) {
  let timeout
  return () => {
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout({
      fn
    }, time)
  }
}

建议: 面试的时候先写出如上形式, 如果有时间再考虑实现带 immediate 形式的防抖函数。

节流: 在指定时间内多次触发事件只执行一次(适用于连续的事件, 比如 scroll)

function throttle(fn, time) {
  let preTime = 0
  return () => {
    const remainTime = time - (Date.now() - preTime)
    if (remainTime <= 0) {
      fn()
      preTime = Date.now()
    }
  }
}