好似没有封面

你不知道的Javascript(上卷)阅读笔记

·
20 分钟阅读

第一部分 作用域与闭包

第一章 作用域

作用域是用来存储变量的空间,作者从编译层面介绍了LHS(左查询)和RHS(右查询)的概念。 LHS指找到变量的容器本身,从而对其赋值,而RHS是要找到这个变量的值。例如var a = b,其中会对a进行LHS,为了获取b的值从而赋给a,对b进行右查询。
区分左右的意义在于,进行RHS时,顺着作用域寻找这个值是如果在作用域链没找到则会抛出引用错误(Reference Error), 而在非严格模式下,进行LHS时,如果变量没找到,则它会在顶层作用域里创建这个变量。这就解释了为什么我们直接执行 a = 2时,如果没有a这个变量,会在顶层作用域里存在a这个变量。

第二章 词法作用域

在编译阶段的一部分工作就是找到所有的声明并关联相应的作用域,词法作用域就是由书写代码时函数声明的位置决定的。
可以通过eval()和with来欺骗作用域,在运行时创建作用域,但是副作用是巨大的,会造成严重性能问题(引擎优化失效),应避免使用。

第三章 函数作用域与块作用域

函数作用域能够很好的将变量或函数隐藏起来,这符合最小暴露原则的软件设计思想,在代码中可以使用立即执行函数(IIFE)使代码更清晰。 在ES6之前JS不存在块作用域,if中使用var声明的变量会被提升到外层作用域,在for中的循环变量通常会污染作用域,ES6中的let/const带来很大的好处,它将变量限定在了声明的块作用域中。

第四章 提升

变量和函数在内的所有声明都会在代码执行前被处理,这就是变量提升。var a = 2,这段代码在处理时会被JS看作两部分,var aa=2, 变量声明会被提升到作用域顶部,而赋值停留在原地。
对于函数而言,函数声明会提升,而函数表达式不会。其中存在函数优先原则:函数首先被提升,其次才是变量。重复var声明会被忽略,从中得出的结论是避免使用重名变量以及再块内声明函数,这通常会造成不可预知的结果。

第五章 闭包

闭包是一个在JS中被神话了的概念,根据文中的解释,当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。闭包的一个重要应用就是实现模块化,这种实现不能被静态识别(编译时),而ES6引入了ESM,从语言层面支持了模块化,能够静态分析依赖。

附录

在ES6之前实现块级作用域的方式是使用try catch,所以很多工具为了兼容ES5,转化完的结果有很多奇怪的try catch。

第二部分 this和原型对象

第一章 关于this

this的只想既不指向函数自身,也不指向函数的词法作用域。this实在运行时进行绑定的,具体来说就是在函数被调用时会创建一个执行上下文,其中就有this属性,会在函数执行中用到。

第二章 this全面解析

调用栈

调用栈指的是为了到达当前位置所调用的所有函数。

  1. 工作原理:
  • 调用函数: 当一个函数被调用时,一个包含函数调用信息的栈帧(Stack Frame)被推入调用栈。
  • 执行函数: 被调用的函数开始执行,并可以调用其他函数。
  • 返回: 当函数执行完成时,对应的栈帧从调用栈中弹出,控制权返回给调用该函数的上下文。
  1. 调用栈的特性:
  • 后进先出(Last In, First Out - LIFO): 调用栈是一个后进先出的数据结构,最后被推入栈的栈帧最先被弹出。
  • 同步执行: JavaScript 是单线程的,调用栈是同步执行的,一次只能执行一个栈帧。
this绑定规则
  1. 默认绑定: this默认指向全局对象global/window,当直接调用函数时函数的this默认指向全局对象,严格模式下指向undefined;
  2. 隐式绑定:当函数引用有上下文对象时,指向这个对象。当obj.fn被赋值给其他变量时,是传递了函数本身而不是obj.fn,因此会丢失绑定,变成默认绑定。
    function foo() {
        console.log(this.a);
    }
    let obj = {
        a = 1,
        foo: foo,
    }
    let a = "globe"
    setTimeout(obj.foo, 100); // "globe"
  1. 显式绑定: JS的所有函数原型上都有call,apply,bind方法用来显式绑定this。使用硬绑定bind会返回一个绑定了this的函数,bind的原理时创建一个包装函数,函数内部每次执行fn.apply(obj, argument);
  2. new 绑定: 使用new调用构造函数把属性绑定到创建的对象身上。
    绑定优先级:new > 显式 > 隐式 > 默认。
    bind源码中底层做了判断,如果是被new调用的话会使用新创建的this替换硬绑定的this,这么做的目的是在new的时候可以传入一个已经附带好很多默认参数的函数,这样只需要初始化其余参数,实现一种“柯里化”。

绑定例外

把null或者undefined作为this绑定的对象传入call、apply或者bind,应用的时默认绑定规则。 我可以通过实现一种软绑定,使得将函数的默认绑定指向指定的对象,从而增加灵活性,以下是支持柯里化的软绑定实现:

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        var curried = [].slice.call(arguments, 1);
        var bound = function() {
            return fn.apply((!this || this === (window || global)) ? obj : this, curried.concat.apply(curried, arguments));
        }
        bound.prototype = Object.create(fn.prototype);
        return bound;
    }
}

this词法

箭头函数的this指向其所在的词法作用域,箭头函数没有自己的this,他会捕获所在词法作用域的this,因此它的this时无法修改的,new也不行!

第三章 对象

typeof null的结果是object因为js底层将前三位二进制为0的数据判断为对象,而null为全0。
对象的属性名永远都是字符串,使用其他类型作为属性会被转为字符串,在ES6中可以使用计算属性名[]。

对象复制

对象复制并不是一个简单的问题,如果是浅拷贝,那么属性可以指向共同引用,可以通过循环实现,ES6中提供了Object.assign(target, source)方法实现浅拷贝,其中target是复制的目标,js会遍历source中的所有可枚举属性(enumable),复制到targer身上。
深拷贝实现更为复杂,lodash提供了deepclone方法,另外对于JSON安全的对象可以使用JSON.parse(JSON.stringify(obj))来实现复制,实际上JSON.parse的第二个参数是一个回调函数可以控制遇到循环引用时做的操作。以下是面试版深拷贝的实现:

function deepClone(obj, mp = new WeakMap()) {
    // 创建一个新的对象
    let newObj = Array.isArray(obj) ? [] : {};
    // 遍历原对象的属性
    for (let prop in obj) {
        // 如果是原始值类型,直接赋值
        if (typeof obj[prop] !== 'object') {
            newObj[prop] = obj[prop];
        } else {
            // 如果是引用类型,递归调用
            // 如果已经存在,直接返回
            if (mp.has(obj[prop])) {
                return mp.get(obj[prop])
            }
            // 如果不存在,递归调用
            newObj[prop] = deepClone(obj[prop], mp);
        }
    }
    return newObj;
}
访问对象属性

访问对象时对象不存在会到原型链上找,可以给属性设置get和set。当我们访问一个属性时可能这个属性的值是undefined也有可能是因为不存在属性返回undefined,为了区分可以通过判断属性是否存在来区分。可以使用prop in obj(in操作符)或Object.hasOwnProperty(prop)来判断,它们的区别是in操作符会追溯原型链,而后者不会。

遍历对象

可以使用for...in...来遍历所有对象身上的可枚举属性,尽量避免用for in去遍历数组,因为可能数组身上存在其他可枚举属性,进而存在潜在问题。在ES6中新增了for...of...可以用来值遍历,通过可迭代对象身上的[Symbol.Iterator],调用迭代器对象身上的next方法实现遍历逻辑。

第四章 混合对象“类”

前面大部分篇幅在介绍面对对象的思想,然而在JS当中实际上并没有类,通常类继承所做的是复制,而由于JS中都是对象,所以JS继承实际上是对象之间的关联
开发者为了在js中实现类似的复制行为,相出了mixin(混入)来实现复制。
显式mixin:

function mixin(sourceObj, targetObj) {
    for (var key in sourceObj) {
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

实际上就是遍历对象然后复制到目标对象,属性会被添加,但是对象引用是相同的。
在JS中尝试这种模拟类往往是得不偿失的,虽然能够解决一些问题,但是同样也会引入更多的问题。

第五章 原型