《JavaScript语言精粹》阅读小记

88 阅读5分钟

1. 精华

2. 语法

1. 若数字字面量有指数部分,该字面量的值等于e之前的数字与10的e之后的数字的次方相乘,例如1e2(1 * 10 ^ 2) === 100
2. 字符串是不可变的
let str = 'javascript'
// str[0] == 'j'
str[0] = 't' // 不生效
str = 'helloWorld' // 可生效,但这里在栈内存开辟了新地址,不是改变旧地址中的值
3. 被判定为false的值(其余均为true)
  • false
  • null
  • undefined
  • 空字符串''
  • 数字0
  • 数字NaN
4. try、catch、throw和finally
// 栗子1:若throw在try中,执行后控制流跳转到对应catch中
try {
  throw (...) // 抛出的异常。可以是字符串、数字、逻辑值或对象。
} catch(err) {
  // 通常异常包含一个name属性和一个message属性
} finally {
  // 无论结果如何均会执行
}

// 栗子2:若throw在函数中,则函数将中止不会继续执行,控制流跳转至调用该函数的try语句对应的catch中
function UserException(message) {
  this.message = message;
  this.name = "UserException";
}

function getMonthName(mo) {
  mo = mo-1; // 调整月份数字到数组索引 (1=Jan, 12=Dec)
  var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
      "Aug", "Sep", "Oct", "Nov", "Dec"];
  if (months[mo] !== undefined) {
     return months[mo];
  } else {
     throw new UserException("InvalidMonthNo");
     console.log('haha') // 这里不执行
  }
}

try {
  var myMonth = 15; // 15 超出边界并引发异常
  var monthName = getMonthName(myMonth);
  console.log('lala') // 这里不执行
} catch (e) {
  console.log(e)
}

3. 对象

1. 指定某对象作为原型
// 给Object增加一个create方法
Object.create = funciton (obj) {
    var F = function () {}
    F.prototype = obj
    return new F()
}
var newObj = Object.create(oldObj)
2. 原型连接在对象更新时不起作用,检索时才会起作用
// 更新
obj.name = 'ml' // 不会改变obj原型的属性

// 检索
console.log(obj.name) // 若obj中无name属性,会试着在其原型中获取

4. 函数

1. prototype_proto_关系
// _proto_指向的就是他的构造函数的 prototype
function B(name) {
  this.name = name;
}
var b = new B("ml");
// b._proto_ == B.prototype
2. 什么是原型链
function B(name) {
  this.name = name;
}
var b = new B("ml");
// 在该例子中,若调用 b.toString(),由于其本身没有该方法,会在其 _proto_(即构造函数的 prototype)中寻找
// 最后会在Object.prototype中找到 toString()
3. this指向问题
// 函数: 谁调用了我,我里边儿的 this指向谁

// 以函数调用模式(函数形式)调用的函数,this指向全局
function add(a, b) {
    return a + b
}

let myObj = {
    value: 1,
    double: function () {
        let that = this // 若不采用该解决方法,会导致 helper中执行异常,因为 this指向了 window,而不是 myObj
        
        let helper = function () {
            that.value = add(that.value, that.value)
        }
        
        helper() // 以函数形式调用 this指向全局 window
    }
}
myObj.double() // 以方法形式调用,this指向 myObj
4. call、apply、bind你们是干嘛的
三者用于改变函数的this指向
比如:b.call(a);  
// b的 this指向了 a(即 b的 this上下文被改变了,变 a的了,太惨了)
// 补充1:一般多数情况 b为函数, a为对象,调用的是 b函数
// 补充2:以下为 new函数的实现,其中的 o对象在调用 apply后会改变
function Person(first,last) {
  this.first = first;
  this.last = last;
}

function trivialNew(constructor, ...args) {
  var o = {}; // 创建一个对象
  constructor.apply(o, args);
  return o;
}

let bill = trivialNew(Person, "William", "Orange");// 这里即相当于 new函数
// 栗子1(直接理解)
let a = {
  name: 'gulululala', //定义a的属性
  say: function() { //定义a的方法
    console.log("你好啊!");
  }
};
function b(name){
  console.log("我原来叫:"+ name);
  console.log("我现在叫:"+ this.name);
  this.say();
}

b.call(a,'test');
//我原来叫:test
//我现在叫:gulululala
//你好啊!
// 栗子2(后者想用前者的方法)
function Person(name) {
  this.name = name;
}

Person.prototype.showName = function () {
  console.log(this.name);
};

let person = new Person('malie');
person.showName();

let animal = {
  name: 'cat'
};

// 若新定义的 animal对象也想用 person中的 showName方法,有以下三种方法
// 1 call用法
person.showName.call(animal);
// 2 apply用法
person.showName.apply(animal);
// 3 bind用法
person.showName.bind(animal)();

// 用法总结:前两个改变 obj的 this上下文后直接执行,第三个是返回了改变了上下文后的函数
// call与apply用法区别在于apply传的是参数数组
// bind调用参数格式与call相同(逐个调用)
fn.call(obj, arg1, arg2, arg3...);
fn.apply(obj, [arg1, arg2, arg3...]);
5. 闭包栗子
// bad
// 使用这种方式给某个 dom节点组设置点击事件,结果预想是节点的序号
// 但结果是,每个节点总显示的是节点组的总数目
var add_the_handlers = function (nodes) {
  for (var i = 0; i < nodes.length; i += 1) {
    nodes[i].onclick = function () {
      alert(i)
    }
  }
}
// add_the_handlers(document.getElementsByTagName('li'))

产生这种结果的原因是事件处理器函数绑定的是变量i,而不是函数在构造时i的值。或者可以说,对于事件处理函数而言,其内部上下文中并不存在变量i。点击事件执行时,会尝试沿作用域链向父级的作用域中去寻找。而在上一级,也即add_the_handlers这个函数的上下文中,找到了变量i,经循环后i即为nodes的长度

// good
// 使用闭包
var add_the_handlers = function (nodes) {
  for (var i = 0; i < nodes.length; i += 1) {
    nodes[i].onclick = function (e) {
      return function() {
        alert(e)
      }
    }(i)
  }
}

使用闭包可达到预想的效果原因是,事件处理函数绑定的是传递进去的i的值,而不是add_the_handlersi的值

// very good
// ES6可用let实现
var add_the_handlers = function (nodes) {
  for (let i = 0; i < nodes.length; i += 1) {
    nodes[i].onclick = function () {
      alert(i)
    }
  }
}
6. 使用“记忆”优化算法

函数可以使用对象去记住先前操作的结果,从而能避免无谓的运算,这种优化被称为记忆(避免重复演算之前已被处理的输入)

举个栗子:递归计算 Fibonacci数列

let count = 0 // 仅用于计数
let fibonacci = function (n) {
    count++ // 仅用于计数
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}

for(let i = 0; i <= 10; i += 1) {
    console.log(i + ':' + fibonacci(i))
}
console.log(count) // 453 
// 0:0
// 1:1
// 2:1
// 3:2
// 4:3
// 5:5
// 6:8
// 7:13
// 8:21
// 9:34
// 10:55
// 453

以上栗子,我们循环调用了11次,但fibonacci函数被调用了453次,其中大多数均在计算先前已处理过的输入

let count = 0 // 仅用于计数
let fibonacci = function () {
   let memo = [0, 1]
   let fib = function (n) {
       count++ // 仅用于计数
       let result = memo[n]
       if (typeof result !== 'number') {
           result = fib(n - 1) + fib(n - 2)
           memo[n] = result
       }
       return result
   }
   return fib
}()

for(let i = 0; i <= 10; i += 1) {
    console.log(i + ':' + fibonacci(i)) // 结果同上
}
console.log(count) // 29 

优化后的方法,将结果隐藏在闭包中,使用“记忆”的方式记录先前处理过的输入,最后调用29次函数即可或者结果