You Don't Know JS(上)

目录

[TOC]来生成目录:

作用域是什么

1、作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。 赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域 的赋值操作。
2、JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声 明会被分解成两个独立的步骤:
首先,vara在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。 接下来,a=2会查询(LHS查询)变量a并对其进行赋值。
3、LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
4、不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)

function foo(a) { 
console.log( a ); // 2
}
foo( 2 );

最后一行 foo(..) 函数的调用需要对 foo 进行 RHS 引用,意味着“去找到 foo 的值,并把 它给我”。并且 (..) 意味着 foo 的值需要被执行,因此它最好真的是一个函数类型的值

词法作用域

1、词法作用域:词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。
这里写图片描述
2、欺骗词法 eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域

  • eval()
function foo(str, a) {
      eval(str);//欺骗
      console.log(a, b)
}
var b = 2;
foo("var b=3", 1)

eval(..) 调用中的 “var b = 3;” 这段代码会被当作本来就在那里一样来处理。由于那段代 码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改。事实 上,和前面提到的原理一样,这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。

  • 在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。
function foo(str) {
    "use strict";
    eval(str);
    console.log(a); // ReferenceError: a is not de ned
}
foo("vara=2");
  • with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象 本身。
       function foo(obj) {
            with(obj) {
                a = 2;
            }
        }
        varo1 = {
            a: 3
        };
        varo2 = {
            b: 3
        };
        foo(o1);
        console.log(o1.a); // 2
        foo(o2);
        console.log(o2.a); // undefined
        console.log(a); // 2——不好,a 被泄漏到全局作用域上了!

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。

  • 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们

函数作用域和块作用域

函数作用域和块作用域的行为是一样的,可以总结为:任何声明在 某个作用域内的变量,都将附属于这个作用域。

  • 函数作用域
    1、函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)
    典型案例:闭包
    2、规避作用域冲突的方法有:立即执行函数表达式、闭包、var改成let、(命名空间、模块管理也是两种方法)

  • 块作用域
    1、块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。
    2、块作用域的用处:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。举一个反例

 for (var i = 0; i < 10; i++) {
            console.log(i);
        }

我们在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使
用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。

3、let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let 为其声明的变量隐式地了所在的块作用域。
4、使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不 “存在”。

 {
   console.log(bar); // ReferenceError! 
   let bar=2;
 }

5、var在闭包配合使用的时候,很有可能不被垃圾回收,而let为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具 ,配合闭包使用的话,可以让引擎清楚地知道没有必要继续保存 ,进行垃圾回收
6、const同样可以建立块作用域,只是const不可以改变key的value值,但是可以改变对象下的key的value值,例子:

const a={b:2} const a={b:3}修改值成功

  • 动态作用域
    1、动态作用域似乎暗示有很好的理由让作用域作为一个在运行时就被动态确定的形式,而不 是在写代码时进行静态确定的形式,事实上也是这样的
function foo() {
 console.log( a ); // 2
}
function bar() { 
  var a = 3;
  foo(); 
}
var a = 2;
 bar();

词法作用域让 foo() 中的 a 通过 RHS 引用到了全局作用域中的 a,因此会输出 2

2、动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调 用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套

function foo() {
  console.log( a ); // 3(不是2!)
}
function bar() { 
  var a = 3;
  foo();
}
var a = 2; 
bar();

为什么会这样?因为当 foo() 无法找到 a 的变量引用时,会顺着调用栈在调用 foo() 的地 方查找 a,而不是在嵌套的词法作用域链中向上查找。由于 foo() 是在 bar() 中调用的, 引擎会检查 bar() 的作用域,并在其中找到值为 3 的变量 a

3、事实上 JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。 但是 this 机制某种程度上很像动态作用域

主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

提升

1、var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。 这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。 可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的 最顶端,这个过程被称为提升。
2、只有声明本身会被提升,而赋值或其他运行逻辑会留在原地 ,另外值得注意的是,每个作用域都会进行提升操作。

      function foo() {
            var a;
            console.log(a); // undefined
            a = 2;
        }
        foo();

        var a = 2;
        var b = ++a;
        console.log(a, b) //3,3

        var a = 2;
        var b = a++;
        console.log(a, b); //3,2

3、函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个 “重复”声明的代码中)是函数会首先被提升,然后才是变量。

      foo(); // 1
        var foo;

        function foo() {
            console.log(1);
        }
        foo = function () {
            console.log(2);
        };

4、尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

        foo(); // 3
        function foo() {
            console.log(1);
        }
        var foo = function () {
            console.log(2);
        };

        function foo() {
            console.log(3);
        }

作用域闭包

1、当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。
2、模块有两个主要特征:
(1)为创建内部作用域而调用了一个包装函数;
(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。

          function CoolModule() {
            var something = "cool";

            var another = [1, 2, 3];

            function doSomething() {
                console.log(something);
            }

            function doAnother() {
                console.log(another.join(" ! "));
            }
            return {
                doSomething: doSomething,
                doAnother: doAnother
            };
        }
        var foo = CoolModule();
        foo.doSomething(); // cool
        foo.doAnother(); // 1 ! 2 ! 3

关于 this

1、this 既不指向函数自身也不指向函数的词法作用域

        function foo(num) {
            console.log("foo: " + num);
            // 记录 foo 被调用的次数
            this.count++;
        }
        foo.count = 0;
        var i;
        for (i = 0; i < 10; i++) {
            if (i > 5) {
                foo(i);
            }
        }
        // foo: 6
        // foo: 7
        // foo: 8
        // foo: 9
        // foo被调用了多少次?
        console.log(foo.count); // 0 -- WTF?

2、this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

  function foo(num) {
            console.log("foo: " + num);
            // 记录 foo 被调用的次数
            // 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo this.count++;
        }
        foo.count = 0;
        var i;
        for (i = 0; i < 10; i++) {
            if (i > 5) {
      // 使用 call(..) 可以确保 this 指向函数对象 foo 本身
                foo.call(foo, i);
            }
        }
        // foo: 6
        // foo: 7
        // foo: 8
        // foo: 9
        // foo被调用了多少次? console.log( foo.count ); // 4

this 全面解析

1、判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置
2、找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  • 由new调用?绑定到新创建的对象。var bar = new foo()
  • 由call或者apply(或者bind)调用?绑定到指定的对象。 . var bar = foo.call(obj2)
  • 由上下文对象调用?绑定到那个上下文对象。 . var bar = obj1.foo()
  • 默认:在严格模式下绑定到undefined,否则绑定到全局对象。 . var bar = foo()

3、一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑 定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null),以保护全局对象。
4、ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)

对象

1、对象有字面形式(比如var a = { .. })和构造形式(比如var a = new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项
2、对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。
3、对象就是键 / 值对的集合。可以通过 .propName 或者 [“propName”] 语法来获取属性值。访 问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]), [[Get]] 操作会检查对象本身是否包含这个属性,如果没找到的话还会查找 [[Prototype]] 链
4、属性的特性可以通过属性描述符来控制,比如 writable 和 configurable。此外,可以使用 Object.preventExtensions(..)、Object.seal(..) 和 Object.freeze(..) 来设置对象(及其 属性)的不可变性级别。
5、属性不一定包含值——它们可能是具备 getter/setter 的“访问描述符”。此外,属性可以是 可枚举或者不可枚举的,这决定了它们是否会出现在 for..in 循环中

混合对象“类”

1、类意味着复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。
2、多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果
3、JavaScript 并不会(像类那样)自动创建对象的副本
4、混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this, …)),这会让代码更加难 懂并且难以维护。

function mixin(sourceObj, targetObj) {
            for (var key in sourceObj) { 
            // 只会在不存在的情况下复制 
                if (!(key in targetObj)) {
                    targetObj[key] = sourceObj[key];
                }
            }
            return targetObj;
        }
        var Vehicle = {
            engines: 1,
            ignition: function () {
                console.log("Turning on my engine.");
            },
            drive: function () {
                this.ignition();
                alert("Steering and moving forward!");
            }
        };
        var Car = mixin(Vehicle, {
            wheels: 4,
            drive: function () {
                Vehicle.drive.call(this);
      alert("Rolling on all " + this.wheels + " wheels!");
            }
        });
        console.log(Car)

此外,显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也 是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多 问题。 总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋 下更多的隐患。

原型

  • [[Prototype]]机制就是一个对象的内部链接引用另外一个对象,本质就是对象之间的关联关系

所有普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如
果在原型链中找不到指定的属性就会停止。toString()、valueOf() 和其他一些通用的功能 都存在于
Object.prototype 对象上,因此语言中所有的对象都可以使用它们。

  • 属性设置和屏蔽
       var anotherObject = {
            a: 2
        };
        var myObject = Object.create(anotherObject);
        anotherObject.a; // 2
        myObject.a; // 2
        anotherObject.hasOwnProperty("a"); // true
        myObject.hasOwnProperty("a"); // false
        myObject.a++; // 隐式屏蔽! anotherObject.a; // 2 
        myObject.a; // 3
        myObject.hasOwnProperty("a"); // true
  • constructor 并不表示被构造 .constructor 并不是一个不可变属性。它是不可枚举(参见上面的代码)的,但是它的值 是可写的(可以被修改)。
       function Foo() { /* .. */ }
        Foo.prototype = { /* .. */ }; // 创建一个新原型对象
        var a1 = new Foo();
        a1.constructor === Foo; // false
       a1.constructor === Object; // true!

a1 并没有 .constructor 属性,所以它会委托 [[Prototype]] 链上的 Foo. prototype。但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这 个属性!),所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象 有 .constructor 属性,指向内置的 Object(..) 函数

  • 使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样
    Object.create(…)
function Foo(name) {
            this.name = name;
        }
        Foo.prototype.myName = function () {
            return this.name;
        };

        function Bar(name, label) {
            Foo.call(this, name);
            this.label = label;
        }
// 我们创建了一个新的 Bar.prototype 对象并关联到Foo.prototype 
        Bar.prototype = Object.create(Foo.prototype);
// 注意!现在没有 Bar.prototype.constructor 了 如果你需要这个属性的话可能需要手动修复一下它
        Bar.prototype.myLabel = function () {
            return this.label;
        };
        vara = newBar("a", "obja");
        a.myName(); // "a"
        a.myLabel(); // "obj a"

虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但 是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。 创建一个合适的关联对象,我们必须使用 Object.create(..) 而不是使用具有副 作用的 Foo(..)。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能 直接修改已有的默认对象。

  • 属性判断的几种方式检查“类”关系

a instanceof Foo; // true
Foo.prototype.isPrototypeOf( a ); // true
b.isPrototypeOf( c );
Object.getPrototypeOf( a ) === Foo.prototype; // true

行为委托

对象关联风格代码和行 为委托设计模式

  • 类与[[Prototype]]区别

传统面向类的语言中父类和子 类、子类和实例之间其实是复制操作,但是在 [[Prototype]] 中并没有复制,相反,它们 之间只有委托关联。

这里写图片描述
这里写图片描述
- 匿名函数缺点
匿名函数没有 name 标识符,这会导致:
1. 调试栈更难追踪;

2.自我引用(递归、事件(解除)绑定,等等)更难;
3. 代码(稍微)更难理解。

  • 自省

a instanceof Foo; // true
Foo.prototype.isPrototypeOf( a ); // true
b.isPrototypeOf( c );
Object.getPrototypeOf( a ) === Foo.prototype; // true 推荐使用

ES6 中的 Class

  • 优点
    1. (基本上,下面会详细介绍)不再引用杂乱的 .prototype 了。
    2. Button声明时直接“继承”了Widget,不再需要通过Object.create(..)来替
      换 .prototype 对象,也不需要设置 .proto 或者 Object.setPrototypeOf(..)。
    3. 可以通过super(..)来实现相对多态,这样任何方法都可以引用原型链上层的同名方 法。这可以解决第 4 章提到过的那个问题:构造函数不属于类,所以无法互相引用——
      super() 可以完美解决构造函数的问题。
    4. class字面语法不能声明属性(只能声明方法)。看起来这是一种限制,但是它会排除
      掉许多不好的情况,如果没有这种限制的话,原型链末端的“实例”可能会意外地获取 其他地方的属性(这些属性隐式被所有“实例”所“共享”)。所以,class 语法实际上 可以帮助你避免犯错。
    5. 可以通过extends很自然地扩展对象(子)类型,甚至是内置的对象(子)类型,比如 Array 或 RegExp。没有 class ..extends 语法时,想实现这一点是非常困难的,基本上 只有框架的作者才能搞清楚这一点。但是现在可以轻而易举地做到!
  • 缺点

也就是说,class 并不会像传统面向类的语言一样在声明时静态复制所有行为。如果你 (有意或无意)修改或者替换了父“类”中的一个方法,那子“类”和所有实例都会受到
影响,因为它们在定义时并没有进行复制,只是使用基于 [[Prototype]] 的实时委托

     class C {
            constructor() {
                this.num = Math.random();
            }
            rand() {
                console.log("Random: " + this.num);
            }
        }
        var c1 = new C();
        c1.rand(); // "Random: 0.4324299..."
        C.prototype.rand = function () {
console.log("Random: " + Math.round(this.num * 1000));
        };
        var c2 = new C();
        c2.rand(); // "Random: 867"
        c1.rand(); // "Random: 432" ——噢!

class 语法无法定义类成员属性(只能定义方法),如果为了跟踪实例之间共享状态必须要 这么做,那你只能使用丑陋的 .prototype 语法,像这样:

 class C {
            constructor() {
// 确保修改的是共享状态而不是在实例上创建一个屏蔽属性! C.prototype.count++;
// this.count可以通过委托实现我们想要的功能
                console.log("Hello: " + this.count);
            }
        }
// 直接向 prototype 对象上添加一个共享状态 C.prototype.count = 0;
        var c1 = new C(); // Hello: 1
        var c2 = new C(); // Hello: 2
        c1.count === 2; // true
        c1.count === c2.count; // true

此外,class 语法仍然面临意外屏蔽的问题:

        class C {
            constructor(id) {
                // 噢,郁闷,我们的 id 属性屏蔽了 id() 方法
                this.id = id;
            }
            id() {
                console.log("Id: " + id);
            }
        }
        var c1 = new C("c1");
        c1.id(); // TypeError -- c1.id现在是字符串"c1"

除此之外,super 也存在一些非常细微的问题。你可能认为 super 的绑定方法和 this 类似 (参见第 2 章),也就是说,无论目前的方法在原型链中处于什么位置,super 总会绑定到
链中的上一层。
然而,出于性能考虑(this 绑定已经是很大的开销了),super 并不是动态绑定的,它会在 声明时“静态”绑定。没什么大不了的,是吧?

猜你喜欢

转载自blog.csdn.net/michellezhai/article/details/80988216