编程语言
首页 > 编程语言> > JavaScript深入系列-冴羽博客读后总结

JavaScript深入系列-冴羽博客读后总结

作者:互联网

最近再次拜读冴羽大佬的博客,收益颇多。第一次读的时候有点囫囵吞枣,很多不清楚。这次把重要内容做了简单的总结,方便回顾。

1. 原型、原型链

2. 词法作用域和动态作用域

作用域

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

静态作用域与动态作用域

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的

3. 执行上下文栈

当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

对于每个执行上下文,都有三个重要属性:

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。 当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

4. 变量对象

变量对象与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

5. 作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

5.第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

6. this

基于ECNAscript标准解析this指向的问题。

Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。 Reference 的构成,由三个组成部分,分别是:

1.计算 MemberExpression 的结果赋值给 ref

2.判断 ref 是不是一个 Reference 类型

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

2.3 如果 ref 不是 Reference,那么 this 的值为 undefined

7. 闭包

​ 理论角度:

闭包是指那些能够访问自由变量的函数。

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

从实践角度:以下函数才算是闭包:
  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

让我们先写个例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

闭包执行过程中,创建他的上下文已经销毁了。但是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

8. 参数按值传递

ECMAScript中所有函数的参数都是按值传递的。

两个复杂结构传递的例子:

var obj = {
    value: 1
};
function foo(o) {
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1

注意: 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!

所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。

最后,你可以这样理解:

  1. 参数如果是基本类型是按值传递,如果是引用类型按共享传递。

  2. 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。

9. call 和 apply的模拟实现

call的概念:

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

举个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

注意两点:

  1. call 改变了 this 的指向,指向到 foo
  2. bar 函数执行了

实现总体思路:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数

其他细节:

  1. 传参:使用数组获取arguments,并使用eval函数执行;
  2. 返回值,将结果返回。

10. bind实现

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

由此我们可以首先得出 bind 函数的两个特点:

  1. 返回一个函数
  2. 可以传入参数

总体思路:

  1. 返回一个函数,函数中使用apply传入this指向的对象;
  2. 其他参数通过arguments拼接
  3. 可以new的构造函数通过修改返回的函数的原型来实现。修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值

11. new 的模拟实现

使用 new 操作符,调用构造函数会执行如下操作:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性(获取原型上的属性和方法)。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象) -> 获取构造函数内的属性和方法。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

12. 类数组对象与arguments

所谓的类数组对象:

拥有一个 length 属性和若干索引属性的对象

类数组转数组的方法:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

Arguments 对象的 callee 属性,通过它可以调用函数自身。

13. 创建对象的多种方式以及优缺点

  1. 工厂模式。在函数内new一个object对象,然后设置各个属性

    缺点:对象无法识别,因为所有的实例都指向一个原型

  2. 构造函数模式

    优点:实例可以识别为一个特定的类型

    缺点:每次创建实例时,每个方法都要被创建一次

  3. 原型模式。 属性和方法都写在构造函数的原型上

    优点:方法不会重新创建

    缺点:1. 所有的属性和方法都共享 2. 不能初始化参数

  4. 组合模式,构造函数模式与原型模式双剑合璧。

    优点:该共享的共享,该私有的私有,使用最广泛的方式

    缺点:有的人就是希望全部都写在一起,即更好的封装性

14. 继承的多种方式和优缺点

  1. 原型链继承,将子元素的原型指向父元素;

    缺点:引用类型的属性被所有实例共享,

    ​ 在创建 Child 的实例时,不能向Parent传参

  2. ****借用构造函数****(经典继承) 改变this执行,借用方法

    function Parent () {
        this.names = ['kevin', 'daisy'];
    }
    function Child () {
        Parent.call(this);
    }
    

    优点:

    1.避免了引用类型的属性被所有实例共享

    2.可以在 Child 中向 Parent 传参

    缺点:

    方法都在构造函数中定义,每次创建实例都会创建一遍方法。

  3. ****组合继承****,原型链继承和经典继承双剑合璧。变量写在构造函数里,方法写在原型链上。

  4. 原型式继承

    //就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
    function createObj(o) {
        function F(){}
        F.prototype = o;
        return new F();
    }
    

    缺点:

    包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

标签:执行,冴羽,函数,作用域,JavaScript,对象,scope,上下文,读后
来源: https://blog.csdn.net/zzu_Flyer/article/details/116353395