红皮书里的细节

理念: 尽信书则不如无书

重载

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()
}
}
}