【JS系列1】作用域、this、原型

183 阅读20分钟

作用域

基本描述

相关方

  • 引擎: 负责JS代码的编译及执行过程
  • 编译器: 负责语法分析及代码生成等
  • 作用域:负责收集并维护变量组成的一系列查询即根据变量查找变量的一套规则

编译步骤

  • 分词/词法分析:将语句分解为有意义的代码块
  • 解析/语法分析: 将词法单元流转换成抽象语法树
  • 代码生成:将AST转换成可执行代码(机器指令)

作用域查询标识符(变量)

  • LHS:

    • 赋值操会导致LHS查询,= 操作符或调用函数时传入参数的操作都会导 致关联作用域的赋值操作。
    • 未声明变量时,会导致自动隐式创建一个全局变量,严格模式下报错。
  • RHS:

    • 获取变量值导致RHS查询。
    • 未声明变量时,会抛出ReferenceError异常。
    • 针对非函数类型数据进行函数操作、null或者undefined获取属性操作,会抛出TypeError异常。

作用域链

作用域链是基于调用栈,而不是代码中作用域嵌套。

词法作用域

  • 词法作用域就是定义在定义词法阶段的作用域,由函数被声明时所处的位置决定。
  • 作用域查找会在找到第一个匹配的标识符时停止。
  • 全局变量会自动变成全局对象的属性(window.XXX)。
  • 词法作用域查找只会 查找一级标识符,比如a 、b 和c 。如果代码中引用了 foo.bar.baz ,词法作用域查找只会试图查找foo 标识符,找到这个变量 后,对象属性访问规则会分别接管对bar 和baz 属性的访问。

运行时修改词法作用域 (导致性能下降)

  • 字符串转为可执行代码
    • eval 动态插入js代码

         function foo(str, a) { 
          eval( str ); // 欺骗! 
          console.log( a, b ); 
         }
         var b = 2; 
         foo( "var b = 3;", 1 ); // 1, 3 
      
    • setTimeout、setTimeInterval第一个参数等

  • with
    • 实际上根据传递对象会凭空创建了一个全新的词法作用域

函数作用域&块作用域

匿名函数缺点

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  • 如果没有函数名,当函数需要引用自身时只能使用已经过期 的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例 子,是在事件触发后事件监听器需要解绑自身。
  • 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的 名称可以让代码不言自明。

常用可规避命名冲突方法

 - 全局命名空间
 - 模块管理

区分函数声明&函数表达式

function foo() {
    var a = 3; 
    console.log( a );
}


(funtion foo() {
    var a = 3;
    console.log(a)
})()
  • function 关键字出现在 声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一 个函数表达式。
  • 名称标识符所在的作用域不同
    • 第一段foo被绑定在所在作用域,可通过foo()调用
    • 第二段foo被绑定在函数表达式自身的函数中,而不是所在作用域

函数作用域

匿名&具名

  • 匿名

    setTimeout( 
        function() { 
            console.log("I waited 1 second!"); 
         }, 1000 
    );
    

    函数表达式可以是匿名的,函数声明则不可以省略函数名(非法)。

    • 缺点

    1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
    2. 如果没有函数名,当函数需要引用自身时只能使用已经过期 的 arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例 子,是在事件触发后事件监听器需要解绑自身。
    3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的 名称可以让代码不言自明。
  • 具名

setTimeout( 
 function timeoutHandler () { 
     console.log("I waited 1 second!"); 
  }, 1000 
);

立即执行函数(IIFE)

```
(function(){console.log("我是匿名函数。")}())
(function(){console.log("我是匿名函数。")})()
!function(){console.log("我是匿名函数。")}()
+function(){console.log("我是匿名函数。")}()
-function(){console.log("我是匿名函数。")}()
~function(){console.log("我是匿名函数。")}()
void function(){console.log("我是匿名函数。")}()
new function(){console.log("我是匿名函数。")}()
```

函数被()包含或者其他的运算符号操作,因此变成了一个表达式,再通过末尾的()立即执行匿名函数。第一种只是将形式红用来调用的()移进了包装的()中

* IIFE传参
    ```
        var a = 2; 
        (function IIFE( global ) { 
            var a = 3; console.log( a ); // 3 
            console.log( global.a ); // 2 
         })( window ); 
         console.log( a ); // 2
    ```
   
* IIFE 倒置代码执行顺序, 将需要运行的代码放置在第二位
    ```
        var a = 2; 
        (function IIFE( def ) { 
            def( window ); 
        })(function def( global ) { 
            var a = 3; 
            console.log( a ); // 3 
            console.log( global.a ); // 2 
        });
    ```
 

块作用域

实现块作用域的方法

  • with
  • try/catch 声明的变量仅在catch中有效
        try {
            undefined()
        } catch(err) {
            console.log(err)
        }
        console.log(err) // ReferenceError: err is not defined
    
  • let
    • 可以将变量(隐式)绑定到所在的任意作用域中{}
    • 使用let 进行的声明不会在块作用域中进行提升
          {
              console.log(bar); // ReferenceError
              let bar =2
          }
      
  • const
    • 创建块作用域常量

提升

  • 只有声明变量本身会被提升,而复制或其他运行逻辑会留在原地。
  • 函数声明会被提升,函数表达式不会。
  • 函数与变量为同一个变量名时,函数会优先被提升,才是变量。

作用域闭包

闭包是基于词法作用域书写代码时所产生的自然结果。

定义

  • 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
  • 函数在定义以外的地方被调用,闭包使函数可以继续访问定义时的词法作用域。
function foo () {
    var a = 2
    function bar () { // 可以访问外部作用域中的变量a 但并不是真正意义上的闭包
        console.log(a)
    }
    bar() 
}
foo()
function foo () {
    var a = 2
    function bar () { // 可以访问外部作用域中的变量a 但并不是真正意义上的闭包
        console.log(a)
    }
    return bar() 
}
var baz = foo();
baz() // 2 闭包效果
  • bar()词法作用域能访问foo()内部作用域,并将bar()函数本身放做一个值进行传递
  • foo()执行后,返回值(内部bar()函数)赋值给变量baz并进行调用,实际上就是通过不同的标识符调用bar()
  • bar()在自己定义的词法作用域中正常执行
  • 正常情况下,foo()执行后,通常会期待foo()整个内部作用域被销毁。但是闭包会组织作用域销毁,实际上foo()内部作用域依然存在,因为bar()本身仍在使用。
  • 作用域由于是由声明位置所定,所以它拥有涵盖foo()内部作用于的闭包,使得foo()作用域一直存在,以供bar()后续使用,那么bar()依然持有对改作用域的引用,而这个引用称为闭包

常用闭包形式

无论使用何种方式对函数类型的值进行传递 ,当函数在别处被调用时 都可以观察到闭包。

function foo () {
    var a = 2
    function baz () { // 可以访问外部作用域中的变量a 但并不是真正意义上的闭包
        console.log(a)
    }
    bar(baz) // 把内部函数baz 传递给bar ,当调用这个内部函数时(现在叫作fn ),它 涵盖的foo() 内部作用域的闭包就可以观察到了,因为它能够访问a 。 
}
function bar (fn) {
    fn()
}
foo()

间接传递函数

var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log( a );
    } 
    fn = baz; // 将baz分配给全局变量 
  } 
 function bar() {
     fn(); // 妈妈快看呀,这就是闭包! 
 }
 foo();
 bar();

无论通过何种手段将内部函数传递 到所在的词法作用域以外,它都会持有对 原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

本质上无论何时何地 ,如果将函数(访问它们各自的词法作用 域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应 用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何 其他的异步(或者同步)任务中,只要使用了回调函数 ,实际上就是在使用 闭包!

常见考点

* 以下函数的执行结果以及原因
```
for (var i=1; i<=5; i++) { 
    setTimeout( function timer() { 
        console.log( i ); }, i*1000 ); 
 }
```
五个六,延迟函数在循环后执行,整个作用域中只有一个ii值为6故会输出56
```
for (var i=1; i<=5; i++) {
    (function() {
        setTimeout( function timer() {
            console.log( i ); }, i*1000 ); 
      })(); 
 }
```
五个6,IIFE虽然封了一个闭包,但是作用域为空,访问i时还是根据词法作用域规则访问全局i。

```
for (var i=1; i<=5; i++) {
    (function() {
        var j = i;
        setTimeout( function timer() {
            console.log( j ); }, j*1000 ); 
      })(); 
 }

 for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j ); }, j*1000 ); 
      })(i); 
 }
```
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回 调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正 确值的变量供我们访问。
```
for (var i=1; i<=5; i++) { 
    let j = i; // 块作用域
    setTimeout( function timer() { 
        console.log( j ); }, j*1000 ); 
 } 
```
```
for (var i=1; i<=5; i++) { 
    setTimeout( function timer() { 
        console.log( i ); }, i*1000 ); 
 }
```
for 循环头部的let声明使变量在每次迭代中都被声明,并且每次迭代使用上一个迭代结束是的值来初始化i

模块

模块模式的必要条件

  • 必须有外部的封闭函数,该函数必须至少被调用一次
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在是有作用域中形成闭包,并且能访问或者修改私有的状态

模块模式用法

var foo = (function CoolModule(id) {
    function change() {
        // 修改公共API publicAPI.identify = identify2;
    }
    function identify1() {
        console.log( id ); 
    }
    function identify2() {
        console.log( id.toUpperCase() );
    }
    var publicAPI = {
        change: change,
        identify: identify1
    };
    return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

命名将要作为公共API返回的对象,通过在模块实例的内部保留对公共API对象内部引用,可以从内部对模块实例进行修改,包含添加、删除、修改。

this和对象原型

this 提供了一种更优雅的方式隐式传递对象引用。

this

运行时绑定,上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 函数被调用时,JS引擎会自动创建执行上下文,执行上下文包含调用栈、调用方法、传入参数等。this只是其中一个属性。

  • this的误解 this 和 词法作用域查找不能混合使用
    • this不指向自身
    • this不指向函数作用域
      • this 在任何情况下都不指向函数的词法作用域

函数调用栈&调用位置

    function baz () {
        console.log("当前调用栈:baz")
        bar(); 
        console.log("bar调用位置")
    }

    function bar () {
        console.log("当前调用栈:baz -> bar")
        foo()
        console.log("foo调用位置")
    }

    function foo () {
        console.log("当前调用栈:baz -> bar -> foo")
        console.log("foo")
    }

    baz() // baz调用位置

绑定规则

先找到调用位置,再判断为下列四则规则中的哪一条。当满足多条规则时再根据优先级排列。

默认绑定

    function foo () {
        console.log(this.a) // 当foo 调用时被解析为全局变量啊,this进行了默认绑定,this指向去全局对象
    }
    var a = 2
    foo() // 2

声明在全局作用域中的a变量其实就是全局对象下的一个同名属性,两者本质上为同一个东西。 严格模式下, 全局对象无法默认绑定,this为undefined

    function foo () {
        "use strict"
        console.log(this.a) // this undefined
    }
    var a = 2
    foo() // Uncaught TypeError: Cannot read property 'a' of undefined

虽然this绑定规则取决于调用位置,但是只有foo()运行在非严格模式下,默认绑定才能绑定到全局对象,严格模式下与foo()调用位置无关。

    function foo () {
        console.log(this.a) // 当foo 调用时被解析为全局变量啊,this进行了默认绑定,this指向去全局对象
    }
    var a = 2
    ~function () {
        "use strict"
        foo()
    }()

隐式绑定

查看调用位置是否有上下文,或者是否被某个对象拥有或者包含。 隐式绑定规则会把函数调用中的this绑定到这个上下文,在下面例子中this.a === obj.a

    function foo () {
        console.log(this.a) // 当foo 调用时被解析为全局变量啊,this进行了默认绑定,this指向去全局对象
    }
    var a = 2
    var obj  = {
        a: "obj",
        foo: foo
    }
    obj.foo() // "obj" 

对象属性引用链中海油最后一层会影响调用位置

    function foo () {
        console.log(this.a) 
        console.log(this.a === obj1.a) // true
    }
    var a = 2;

    var obj1 = {
        a: "obj1",
        foo: foo
    }
    var obj  = {
        a: "obj",
        foo: foo,
        obj1: obj1
    }

    obj.obj1.foo() // "obj1" 
  • 隐式丢失

    一个最常见的this 绑定问题就是被隐式绑定 的函数会丢失绑定对象,也就 是说它会应用默认绑定 ,从而把this 绑定到全局对象或者undefined 上,取决于是否是严格模式。

    • 常见丢失方式

      参数传递是一种隐式赋值

    function foo () {
         console.log(this.a) 
     }
     var a = "golabl";
    
    
     var obj  = {
         a: "obj",
         foo: foo
     }
    
     var bar = obj.foo // "gobal" 
     bar()
    
    function foo () {
         console.log(this.a) 
     }
     var a = "golabl";
    
     function doFun (fun) {
         fun()
     }
     var obj  = {
         a: "obj",
         foo: foo
     }
    
     doFun(obj.foo) // golbal
    

显示绑定

call & apply

   function foo () {
       console.log(this.a) 
   }
   var obj  = {
       a: "obj"
   }

   foo.call(obj) // obj

当传入一个原始值(字符串、布尔、数字)来当作this的绑定对象,这个原始值会被转换成ta的对象形式(new String()、new Boolean()、 new Number())。通常也称为装箱。

解决丢失绑定

  • 硬绑定 Function.prototype.bind 创建包裹函数,传入所有参数并返回接收到的所有值
function foo (something) {
    console.log(something) // 3
    console.log(this.a) // obj
}
var obj  = {
    a: "obj"
}
var bar = function () {
    return foo.apply(obj, arguments)
}
var baz = foo.bind(obj)

baz(3) 

bar(3)

创建一个可以重复使用的辅助函数

function foo (something) {
    console.log(something) // 3
    console.log(this.a) // obj
}
function bind (fn, obj) {
    return function () {
        return foo.apply(obj,arguments)
    }
}
var obj  = {
    a: "obj"
}
var bar = bind(foo, obj)

bar(3)

API调用上下文

实际上是通过call() 或者 apply 实现了显式绑定

function foo (el) {
    console.log(el, this.a) // 1,obj 2,obj 3,obj
}
var obj = {
    a: "obj"
}
var arr = new Array(1, 2, 3)
arr.forEach(foo, obj)

new 绑定

JS中的"构造函数"只是使用的new操作符时被调用的普通函数。

  • 使用new调用函数时的执行操作
    • 创建一个全新的对象
    • 全新对象会被执行[[原型]]连接
    • 全新对象会被绑定到函数调用的this
    • 如果函数没有返回其他对象,那么new表达式中的函数会自动返回全新对象

规则优先级

new > 显示 > 隐式 > 默认

function foo (somethiong) {
    this.a = somethiong
    console.log(this.a)
}
var obj = {
    foo: foo
}
var obj2 = {}
var obj3 = {}
obj.foo(2) // 隐式绑定 2
console.log(obj.a)

obj.foo.call(obj2, 3) // 显示绑定 3
console.log(obj.a, obj2.a) // 2 3

var bar = new obj.foo(4) // new 4 隐式与new对比
console.log(bar.a) // 4

var baz = foo.bind(obj3) // 硬绑定
baz(5)
console.log(obj3.a) // 5

var bam = new baz(6) // 6 硬绑定与new 对比
console.log(bam.a) // 6
  • new与bind 混合使用 主要目的是预先设置函数中的一些参数,这样在使用new 进行初始化是可以之传入其余参数。bind可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传递给下层函数
function foo(a, b) {
    this.val = a + b
    console.log(`${this.val}: ${a}+${b}`) // a1b1: a1+b1
}
var obj = {}
var bar = foo.bind(obj, 'a1')
var baz = new bar('b1')
console.log(baz) // {"val":"a1b1"}

绑定例外

被忽略的this

当null或者undefined作为this的绑定对象传入call、bind、或者bind中时,这些值会被忽略,实际使用的是默认绑定规则。 同时当函数并不关心this的话,仍然需要传入占位符时,null是一个不错的选择。但是可能会导致一些BUG难以分析和追踪。

function foo(a, b) { 
    console.log("a:" + a + ", b:" + b); 
} // 把数组“展开”成参数 
foo.apply( null, [2, 3] ); // a:2, b:3 
// 使用 bind(..) 进行柯里化 
var bar = foo.bind( null, 2 ); 
bar( 3 ); // a:2, b:3

间接引用

function foo () {
    console.log(this.a)
}
var a = 'global'
var obj1 = {
    a: 'obj1',
    foo: foo
}

var obj2 = {
    a: 'obj2'
}

obj1.foo(); // obj1
(obj2.foo = obj1.foo)() // global 目标函数foo的引用
console.log((obj2.foo = obj1.foo))// foo函数
obj2.foo() // obj2

软绑定

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function (obj) {
        // 当前调用的函数
        var fn = this
        // 获取第一个绑定对象,并获取真正的餐胡
        var temp = Array.prototype.shift.call(arguments)
        var currentA = Array.prototype.slice.call(arguments, 1)
        var bound = function () {
            // 判断是否为全局调用位置,全局调用时使用绑定的obj,否则使用调用隐式绑定的obj
            return fn.apply((!this || this === (window || global)) ? obj : this, currentA.concat.apply(currentA, arguments)) 
        }
        bound.prototype = Object.create(fn.prototype)
        return bound
    }
}

function foo() {
    console.log(this)
    console.log('a:'+this.a)
}

var obj = {a: 'obj'},
    obj1 = {a: 'obj1'},
    obj2 = {a: 'obj2'}

var softFoo = foo.softBind(obj)
softFoo() // a:obj

obj1.foo = foo.softBind(obj);
obj1.foo(); // name: obj1 <---- 看!!! 
softFoo.call(obj2); // name: obj2 <---- 看! 
setTimeout(obj2.foo, 10); // name: obj <---- 应用了软绑定

箭头函数中的this

箭头函数=>根据外层函数或者全局作用域决定this, 根据当前的词法作用局决定this, 会继承外层函数调用的this

function foo () {
    return () => {
        console.log(this.a)
    }
}

var obj = {a: 'obj'},
    obj1 = {a: 'obj1'},
    obj2 = {a: 'obj2'}

var bar = foo.call(obj)
bar.call(obj1) // obj 不是 obj1

foo() 内部创建的箭头函数会捕获调用时foo()的this。虽然foo()的this绑定到obj, bar的this 绑定到obj1;但是箭头函数的绑定无法修改。

  • 常用场景
    • 定时器
    function foo () {
        var self = this
        console.log(this)
        setTimeout(() => {
            console.log(this.a)
        }, 100)
    }
    
    var obj = {a: 'obj'},
        obj1 = {a: 'obj1'},
        obj2 = {a: 'obj2'}
    
    foo.call(obj)
    

Tips

使用self=this和箭头函数本质上是替代this机制, 但是需求注意:

  • 只使用词法作用域并完全抛弃错误的this
  • 完全采用this, 在必要时使用bind, 尽量避免使用self=this

Tips ES6中可以用操作符...来展开数组

更安全的this

传入一个特殊对象,即一个空的非委托对象

创建方法

var ø  = Object.create(null) // 比{}更空不会创建Object.prototype这个委托
console.log(ø, ø.prototype)

对象

基本类型:String、Boolean、Number、Null、Undefined、Symbol

Tips

typeof null 为 object。在底层不同的对象都表示为二进制。在JS中二进制前三位都为0的话会被判为object类型。null的为禁止表示为全0,所以执行typeof时会返回“object”。

内置对象:String、Boolean、Number、Object、Symbol、Function、Array、Date、RegExp、Error

var strPrimitive = "I am a string";
typeof strPrimitive; // "string" 
strPrimitive instanceof String; // false 
var strObject = new String("I am a string");
typeof strObject; // "object" 
strObject instanceof String; // true // 检查sub-type对象 
Object.prototype.toString.call(strObject); // [object String]

原始值"I am a string" 并不是一个对象,它只是一个字面量,并且是一 个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问 其中某个字符等,那需要将其转换为String 对象。引擎自动回吧字面量转换成String对象,所以可以访问属性和方法。

对象赋值

  • JSON.stringify()
  • Object.assign({}) // 浅复制 使用=操作符赋值
var myObj = {
     a: 'obj'
 }
 console.log(Object.getOwnPropertyDescriptor(myObj, 'a'))

 Object.defineProperty(myObj, 'a', {
     value: 'obj',
     writable: true,  // getter setter 
     configurable: true, // 可以从true->false, 不可以从false->true 单向修改
     enumerable: true
 })

 console.log(myObj.a)

保持对象不可变

  • 对象常量 配合使用writable/configurable

  • 禁止扩展 Object.preventEctenSions(...)

  • 密封 Object.seal(...),实现本质是Object.preventEctenSions(...)+所有属性configurable:flase

  • 冻结 Object.freeze(), 实现本质object.seal+writable:false

Getter&Setter

对象默认[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。 在ES5中可以使用getter和setter部分改写默认操作,但只能应用在耽搁属性上,无法应用在整个对象上。当get/set 都有时,JS会忽略它们的value和writeable特性,取而代之的是关心set和get

判断对象是都包含某个属性

  • in
  • obj.hasOwnPrototy()
var myObject = { a:2 }; 

("a" in myObject); // true
 ("b" in myObject); // false 

myObject.hasOwnProperty( "a" ); // true 
myObject.hasOwnProperty( "b" ); // false

in操作符检查属性是否在对象及其[[prototypr]]原型链中。hasOwnPrototype只会检查属性是否在myObject对象中。

  • propertyIsEnumerable(..) 检查属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true。
  • Object.keys(..) 会返回一个数组,包含所有可枚举属性(非原型链上)。
  • Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性, 无论它们是否可枚举(非原型链上)。

遍历

  • for... in ... 遍历对象的可枚举属性列表(包含[[prototype]])
  • ES5 中新增迭代器
    • forEach(..) 会遍历数组中的所有值并忽略回调函数的返回值。
    • every(..) 会一直运行直到回调函数返回false (或者“假”值)。
    • some(..) 会一直运行直到回调函数返回true (或者“真”值)。 every(..) 和some(..) 中特殊的返回值和普通for 循环中的break 语句 类似,它们会提前终止遍历。
  • ES6 for ... of...数组 实质是想被访问对象请求一个迭代器对象,然后通过迭代器对象的next()方法来遍历所有值。
var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();

it.next(); // { value:1, done:false } 

it.next(); // { value:2, done:false } 

it.next(); // { value:3, done:false } 

it.next(); // { done:true }

Tips 在ES6 中使用符号Symbol.iterator来获取对象的@@iterator内部属性。@@iterator本身并不是一个迭代器对象,而是一个返回迭代器对象的函数。

自定义@@iterator

Object.defineProperty( myObject, Symbol.iterator, { 
    enumerable: false, 
    writable: false, 
    configurable: true, 
    value: function() { 
        var o = this; 
        var idx = 0; 
        var ks = Object.keys( o ); 
        return { 
            next: function() { 
                return { 
                    value: o[ks[idx++]], 
                    done: (idx > ks.length) 
                 }; 
            }
        }; 
    } 
});

混合对象"类"

混入、mixin

JS 中的"类"

JS中只有一个近似类的语法元素(new、instanceof、class),但并不是实际的类。在JS中类是一种设计模式,实现近似类的功能。

原型

[[prototype]]

JS中的对象有 [[prototype]] , 其实就是对其他对象的引用,会被自动赋值。

var anotherObject = { a:2 }; 
// 创建一个关联到anotherObject的对象
var myObject = Object.create( anotherObject );
console.log(myObject.a); // 2

Object.prototype

所有普通 的[[Prototype]] 链最终都会指向内置的Object.prototype 。

属性设置&屏蔽

myObject.foo = "bar";

如果属性名foo 既出现在myObject 中也出现在myObject 的 [[Prototype]] 链上层,那么就会发生屏蔽。myObject 中包含的foo 属 性会屏蔽原型链上层的所有foo 属性,因为myObject.foo 总是会选择原型 链中最底层的foo 属性。 屏蔽比我们想象中更加复杂。下面我们分析一下如果foo 不直接存在于 myObject 中而是存在于原型链上层时myObject.foo = "bar" 会出现的 三种情况。

  1. 如果在[[Prototype]] 链上层存在名为foo 的普通数据访问属性(参见 第3章)并且没有被标记为只读(writable:false ),那就会直接在 myObject 中添加一个名为foo 的新属性,它是屏蔽属性 。

  2. 如果在[[Prototype]] 链上层存在foo ,但是它被标记为只读 (writable:false ),那么无法修改已有属性或者在myObject 上创建 屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值 语句会被忽略。总之,不会发生屏蔽。 Tips 只读 属性会阻止 [[Prototype]] 链下层隐式创建(屏蔽)同名属性。这样做主要是为 了模拟类属性的继承。你可以把原型链上层的foo 看作是父类中的属 性,它会被myObject 继承(复制),这样一来myObject 中的foo 属 性也是只读,所以无法创建。但是一定要注意,实际上 并不会发生类似 的继承复制)。这看起来有点奇怪,myObject 对 象竟然会因为其他对象中有一个只读foo 就不能包含foo 属性。更奇怪 的是,这个限制只存在于=赋值中,使用 Object.defineProperty(..) 并不会受到影响。

  3. 如果在[[Prototype]] 链上层存在foo 并且它是一个setter(参见第3 章),那就一定会调用这个setter。foo 不会被添加到(或者说屏蔽于) myObject ,也不会重新定义foo 这个setter。 大多数开发者都认为如果向[[Prototype]] 链上层已经存在的属性 ([[Put]] )赋值,就一定会触发屏蔽,但是如你所见,三种情况中只有一 种(第一种)是这样的。 如果你希望在第二种和第三种情况下也屏蔽foo ,那就不能使用=操作符来 赋值,而是使用Object.defineProperty(..) (参见第3章)来向 myObject 添加foo 。

构造函数

function Foo() { 
    // ... 
} 
console.log(Foo.prototype.constructor === Foo); // true .constructor为默认属性委托指向Foo
var a = new Foo();
console.log(a.constructor === Foo); // true
console.log(Object.getPrototypeOf( a ) === Foo.prototype); // true 

这点代码很容易产生误解认为Foo()是一个构造函数,因为我们使用new 来调用 它并且看到它“构造”了一个对象。

实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函 数,然而,当你在普通的函数调用前面加上new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构 造对象的形式来调用它。同时只是关联并不是复制。

Tips

实际上,对象的.constructor 会默认指向一个函数,这个函数可以通过对 象的.prototype 引用。

常用操作prototype

  • Object.setPrototypeOf()

    关联对象的.prototype

  • obj.isPrototypeOf()

    在对于对象的整条链路上是都出现过obj

  • Object.getPrototypeOf()

    获取对象[[prototypt链]]

  • .proto

    访问._proto_实际上是调用了.proto() (getter函数)。

创建关联

var foo = { 
    something: function() { 
        console.log( "Tell me something good..."); 
     } 
}; 
var bar = Object.create( foo ); 
bar.something(); // Tell me something good...

Object.create(..) 会创建一个新对象(bar )并把它关联到我们指定的 对象(foo ),这样我们就可以充分发挥[[Prototype]] 机制的威力(委 托)并且避免不必要的麻烦(比如使用new 的构造函数调用会生 成.prototype 和.constructor 引用)。

兼容ES5之前

if (!Object.create) { 
    Object.create = function(o) { 
        function F(){} 
        F.prototype = o; 
        return new F(); 
    }; 
}

对象关联

在JavaScript中,[[Prototype]] 机制会把对象关联到其他对象。

function Foo(who) {
    this.me = who;
}
Foo.prototype.identify = function() {
    return "I am " + this.me;
};
function Bar(who) {
    Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function() {
    alert("Hello, " + this.identify() + ".");
};
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak();
b2.speak();

使用对象关联风格优化:

Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};
Bar = Object.create(Foo);
Bar.speak = function() {
    alert("Hello, " + this.identify() + ".");
};
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak();
b2.speak();

Class

class 陷阱

class 基本上只是现有[[Prototype]] (委 托!)机制的一种语法糖。

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

  • 出于性能考虑,super 并不像this 一样是晚绑定 (late bound, 或者说动态绑定)的,它在[[HomeObject]]. [[Prototype]] 上,[[HomeObject]] 会在创建时静态绑定。
class P {
    foo() {
        console.log("P.foo");
    }
}
class C extends P {
    foo() {
        super();
    }
}
var c1 = new C();
c1.foo(); // "P.foo"
var D = {
    foo: function() {
        console.log("D.foo");
    }
};
var E = {
    foo: C.prototype.foo

}; // 把E委托到D 
Object.setPrototypeOf(E, D);
E.foo(); // "P.foo"

在本例中,super() 会调用P.foo() ,因为方法的[[HomeObject]] 仍 然是C ,C.[[Prototype]] 是P 。

确实可以 手动修改super 绑定,使用toMethod(..) 绑定或重新绑定方法 的[[HomeObject]] (就像设置对象的[[Prototype]] 一样!)就可以 解决本例的问题:

var D = {
    foo: function() {
        console.log("D.foo");
    }
}; // 把E委托到 D 
var E = Object.create(D); // 手动把foo的[[HomeObject]]绑定到E,E.[[Prototype]]是D, 所以 super()是D.foo() 
E.foo = C.prototype.foo.toMethod(E, "foo");
E.foo(); // "D.foo"

你不知道的JavaScript中