目录

《你不知道的JavaScript》读书笔记

作用域

  • 作用域是根据名称查找变量的一套规则
  • LHS和RHS引用/查询
  • 变量的赋值操作会执行两个动作,首先编辑器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域查找该变量,如果能够找到就会对它赋值
  • 如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常
  • 严格模式禁止自动或隐式地创建全局变量
  • 词法作用域与动态作用域
  • 词法作用域就是定义在词法阶段的作用域
  • 词法作用域根据词法关系保持书写时的自然关系不变
  • 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
  • 欺骗/修改或创建词法作用域的两种机制eval(..)和with,导致无法在编译时对作用域查找进行优化,性能下降
  • eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码
  • with可以将一个对象处理为一个完全隔离的词法作用域,这个对象的属性也会被处理为定义在这个作用域中的词法标识符
  • 全局变量会自动成为全局对象(比如浏览器中的window对象)的属性
  • 最小特权原则,基于作用域的隐藏变量和函数的技术,设计上将具体内容私有化,不给予外部作用域访问权限,功能性和最终效果都没有受影响
  • 规避标识符冲突,使用作用域来隐藏内部声明,全局作用域声明一个名字足够独特的变量(通常是一个对象),模块管理器
  • 函数声明与函数表达式,如果function是声明中的第一个词,那么就是函数声明,否则就是函数表达式
  • (function foo(){..})作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行,foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域
  • 函数表达式可以是匿名的,函数声明则不可以省略函数名
  • IIFE立即执行函数表达式,(function(){..})()或(function(){..}()),具名和匿名皆可
  • 当时用var声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域
  • 函数作用域与块作用域,函数是JavaScript中最常见的作用域单元
  • let关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部),可隐式或显式地声明块,可用于垃圾回收和循环,块作用域保证变量不会被混乱地复用及提升代码的可维护性
  • const创建块作用域变量的值是固定的(常量)
  • 包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理,这个过程叫作提升,换句话说,现有蛋(声明)后有鸡(赋值)
  • 函数声明会被提升,但函数表达式却不会被提升,由于对undefined值进行函数调用而导致非法操作,抛出TypeError异常,而不是ReferenceError
  • 函数会首先被提升,然后才是变量
  • 函数声明和变量声明重复,变量声明会被忽略。出现在后面的函数声明覆盖前面的函数声明。在同一个作用域中进行重复定义是非常糟糕的
  • 尽可能避免在块(如if块)内部声明函数

闭包

  • 拥有涵盖内部作用域的闭包,使得该作用域能够一直存活,以供在自己定义的词法作用域以外的地方执行,对该作用域的引用就叫闭包(定义)
  • 闭包使得函数可以访问定义时的词法作用域(功能)
  • 无论使用何种方式对函数类型的值进行传递,传递到所在的词法作用域以外,当函数在别处调用时都会使用闭包,可以观察到闭包(过程:定义-传递-调用)
/*
五次六
回调函数在循环结束后才会被执行
行为同语义所暗示的不一致
共享的全局作用域
*/
for (var i=1; i<=5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

/*
五次六
每次迭代创建一个闭包作用域
IIFE通过声明并立即执行一个函数来创建作用域
只是一个什么都没有的空作用域
*/
for (var i=1; i<=5; i++) {
    (function() {
        setTimeout(function timer() {
            console.log(i);
    }, i*1000);
    })();
}

/*
正常
有自己的变量来存储每次迭代的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);
}

/*
正常
let声明变量
*/
for (var i=1; i<=5; i++) {
    let j = i
    setTimeout(function timer() {
        console.log(j);
    }, j*1000);
}

/*
正常,推荐
循环头部的let声明指出变量每次迭代都会声明
每次迭代使用上次迭代结束时的值来初始化变量
*/
for (let i=1; i<=5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}

  • 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问
  • let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量
  • 块作用域和闭包联手
  • 模块模式的两个必要条件:外部函数(调用创建模块实例)和内部函数(在私有作用域中形成闭包,访问和修改私有的状态变量)
  • 函数模块返回对象字面量语法表示的对象或直接返回函数
  • 现代模块机制,外层加上一个友好的包装工具
  • 未来模块机制,一级语法支持,模块系统进行加载时ES6会将文件当做独立的模块来处理,一个文件一个模块
  • 基于函数的模块不能被静态识别,ES6模块API是静态的,在运行时不会改变
  • import..from../module..from../export..,根据需要可多次使用
  • 模块文件中的内容会被当做好像包含在作用域闭包中一样来处理,就和函数闭包模块一样
  • 词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用
  • catch分句可以在ES6之前的环境中作为块作用域的替代方案
/*
let声明会创建一个显式的作用域并与其进行绑定
let定义隐式地劫持一个已经存在的作用域
显式let声明不包含在ES6中
*/
let (a = 2) {
    console.log(a); // 2
}

console.log(a); // ReferenceError

/*
ES6
*/
{
    let a = 2;
    console.log(a);
}

console.log(a);

this绑定

  • this被自动定义在所有函数的作用域中
  • 为什么使用this:函数可以自动引用合适的上下文对象很重要/在不同的上下文对象中重复使用函数
  • this隐式“传递”一个对象引用,无需显式传入一个上下文对象
  • this的两种误解:this指向函数自身(具名函数标识符指向自身/强制this指向函数对象)、this指向函数的词法作用域(任何情况下)
  • 匿名函数无法从函数内部引用自身
  • 当一个函数被调用时,会创建一个活动记录,this就是活动记录的一个属性,this是在运行时(函数被调用时)进行绑定的
  • 分析调用栈,确定调用位置
  • 四种绑定规则:默认绑定(全局对象或undefined,取决于是否是严格模式)、隐式绑定(调用位置是否有上下文对象,被某个对象拥有或包含,会丢失绑定对象)、显式绑定(在对象上强制调用函数而不是在对象内部包含函数引用,调用函数的call(..)和apply(..)方法,硬绑定方法bind(..))、new绑定(构造函数是被new操作符构造调用的普通函数,创建新对象并绑定this)
/*
默认绑定
*/
function foo() {
    console.log(this.a);
}

var a = 2;

foo(); // 2

/*
隐式绑定
函数严格来说不属于obj对象
函数被调用时obj对象拥有或者包含它
*/
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2

/*
隐性绑定丢失绑定对象
bar()其实是一个不带任何修饰的函数调用
*/
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

var bar = obj.foo;

var a = "oops, global";

bar(); // "oops, global"

/*
回调函数丢失绑定对象
参数传递其实就是一种隐性赋值
传入内置函数也同样,如setTimeout
*/
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"

setTimeout(obj.foo, 100); // "oops, global"

/*
显式绑定
对象上强制调用函数
指定this的绑定对象
*/
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2
};

foo.call(obj); // 2

/*
硬绑定
*/
function foo() {
    console.log(this.a);
}

var obj = {
    a: 2
};

var bar = function() {
    foo.call(obj);
};

bar(); // 2
setTimeout(bar, 100); // 2

/*
ES5提供的硬绑定方法
返回一个硬编码的形函数,它会把指定的参数设置为this的上下文
*/
function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}

var obj = {
    a: 2
};

var bar = foo.bind(obj);

var b = bar(3); // 2 3
console.log(b); //5

/*
上下文参数
*/
function foo(el) {
    console.log(el, this.id);
}

var obj = {
    id: "awesome"
};

[1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome

/*
new绑定
所有函数都可以用new来构造调用
*/
function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2
  • 构造函数调用时会自动执行:1. 创建一个全新的对象;2. 这个对象会被执行原型连接;3. 这个对象会绑定到函数调用的this;4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个对象
  • this绑定优先级:new绑定、显式绑定、隐式绑定、默认绑定
  • bin(..)可以把除了第一个参数(用于绑定this的参数)之外的参数都传给下层的函数,进行柯里化
function foo(p1, p2) {
    this.val = p1 + p2
}

var bar = foo.bind(null, "p1");

var baz = new bar("p2");

baz.val; // p1p2
  • polyfill代码主要用于旧浏览器的兼容
  • this的绑定行为存在例外:被忽略的this、函数的间接引用、软绑定
/*
把null或者undefined作为this的绑定对象传入call/apply/bind
实际上应用的是默认绑定
*/
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

/*
更安全的this
把this绑定到一个空的非委托的对象
*/
function foo(a, b) {
    console.log("a:" + a + ", b" + b)
}

// DMZ空对象
var  = Object.create(null);

foo.apply(, [2, 3]); // a:2, b:3

var bar = foo.bind(, 2);
bar(3); // a:2, b:3
  • 软绑定给默认绑定指定一个值,实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力
  • 箭头函数无视this的四种标准规则,而是根据外层作用域来决定this。箭头函数的绑定无法被修改,new也不行。用箭头函数的词法作用域取代了传统的this机制。
/*
箭头函数写法
*/
function foo() {
    setTimeout(() => {
        // 这里的this在词法上继承自foo()
        console.log(this.a);
    }, 100);
}

var obj = {
    a: 2
};

foo.call(obj); // 2

/*
self = this替代this机制写法
*/
function foo() {
    var self = this; // lexical capture of this
    setTimeout( function() {
        console.log(self.a);
    }, 100 );
}

var obj = {
    a: 2
};

foo.call(obj); // 2

对象

  • 基本类型(string/number/boolean/null/undefined)不是对象,typeof null返回object是个bug,因为二进制前三位都为0
  • 函数是对象的一个子类型,是可调用对象,是“一等公民”。数组也是对象的一种类型,具备一些额外的行为。
  • 内置对象(String/Number/Boolean/Object/Function/Array/Date/RegExp/Error)是对象的子类型,实际上只是一些内置函数,可以构造一个对象子类型的新对象
  • 语言会自动把字面量转换为对象,能使用文字形式就不要使用构造形式
  • 要访问myObject中a位置上的值,需要使用.操作符(属性访问)或[]操作符(键访问)
  • 在对象中属性名永远是字符串
/*使用字符串字面量以外的其他值作为属性名,它首先会被转换为字符串*/
var myObject = {};

myObject[myObject] = "baz";

myObject["[object Object]"]; // "baz"
  • ES6增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式来当作属性名
var prefix = "foo";

var myObject = {
    [prefix + "bar"]: "hello"
}

myObject["foobar"]; // hello
  • 函数永远不会属于一个对象,把对象内部引用的函数称为方法有点儿不妥
  • 浅复制只复制引用,深复制可能会出现循环引用导致死循环
  • JSON安全的对象,可以使用var newObj = JSON.parse(JSON.stringify(someObj));巧妙地复制
  • ES6定义了Object.assign(..)方法来实现浅复制var newObj = Object.assign({}, myObject1, myObject2)
  • 从ES5开始,所有的属性都具备了属性描述符,writable(可写)、enumerable(可枚举)、configurable(可配置)三个特性
  • 使用Object.defineProperty添加属性或者修改一个已有属性并对特性进行设置
var myObject = {};

object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});

myObject.a; // 2
  • 只要属性是可配置的,就可以使用defineProperty(..)方法来修改属性描述符
  • 把configurable修改成false是单向操作,无法撤销,并且还会禁止删除(delete)这个属性
  • enumerable控制的是属性是否会出现在对象的属性枚举中
  • 不可变性:对象常量属性(writable: false, configurable: false)、禁止扩展添加新属性(Object.preventExtensions(myObject))、密封(Object.seal(myObject),不能添加和配置或者删除)、冻结(Object.freeze(myObject),不能修改值,最高的浅不变性)
  • 访问描述符
/*getter和setter*/
var myObject = {
    get a() {
        return this.a;
    }

    set a(val) {
        this.a = val * 2;
    }
};

myObject.a = 2;
myObject.a; // 4

/*访问描述符*/
Object.defineProperty(
    myObject,
    "b",
    {
        get: function() { retrun this.a * 2 },
        enumerable: true
    }
);
  • 存在性:in操作符会检查属性是否在对象及其原型链中,hasOwnProperty(..)只会检查属性是否在myObject对象中,Object.prototype.hasOwnProperty.call(myObject, “a”)
  • in操作符检查的是某个属性名是否存在,不可用于数组检查值,也不使用for..in循环遍历数组,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性
  • myObject.propertyIsEnumerable(..)检查属性名是否直接存在于对象上且可枚举,Object.keys(myObject)返回所有可枚举属性,Object.getOwnPropertyNames(..)返回所有属性,均不会查找原型链
  • for..in循环可以遍历对象的可枚举属性列表,包括原型链。遍历对象属性的顺序是不确定的,不同JavaScript引擎中可能不一样,不要相信任何观察到的顺序
  • ES6增加了一种用来遍历数组的for..of循环语法,如果对象本身定义了迭代器的话也可以遍历对象
/*迭代器的使用*/
var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator](); // 获取对象的@@iterator内部属性(返回迭代器对象的函数)

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

for (var v of myArray) {
    console.log(v);
}
// 1
// 2
// 3

/*定义迭代器*/
var myObject = {
    a: 2,
    b: 3
};

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)
                };
            }
        };
    }
} );

  • 类描述了一种代码的组织结构形式——一种在软件中对真实世界中问题的建模方法
  • 数据和操作数据的行为本质上是互相关联的,好的设计就是把它们打包封装起来,也称为数据结构
  • 多态,父类的通用行为可以被子类用更特殊的行为重写
  • 面向对象类的基础上实现了高级设计模式,但类只是一种可选的代码抽象,是一种可选的设计模式
  • 类是DNA,实例是人
  • 类的继承其实就是复制,子类得到的只是父类的一份副本
  • JavaScript中不存在类,只有对象
  • 混入可以模拟类的复制行为,但是通常会产生:显式和隐式
/*显式混入/显式伪多态/相对多态/绝对引用*/
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();
        console.log("Steering and moving forward!");
    }
};

var Car = mixin(Vehicle, {
    wheels: 4,
    drive: function() {
        Vehicle.drive.call(this);
        console.log("Rolling on all " + this.wheels + " wheels!");
    }
});

/*寄生继承*/
function Vehicle() {
    this.engines = 1;
}

Vehicle.prototype.ignition = function() {
    console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function() {
    this.ignition();
    console.log("Steering and moving forward!");
};

function Car() {
    var car = new Vehicle();
    car.wheels = 4;
    var vehDrive = car.drive; // 保存Vehicle::drive()的特殊引用
    car.drive = function() {
        vehDrive.call(this);
        console.log("Rolling on all " + this.wheels + " Wheels!")
    };
    return car;
}

var myCar = new Car(); // new创建的对象会被丢弃,使用返回的car对象

/*隐式混入*/
var Something = {
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1;
    }
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
    cool: function() {
        Something.cool.call(this);
    }
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1
  • 显示混入实际上无法完全模拟类的复制行为,因为对象(包括函数)只能复制引用
  • 显式伪多态在子类方法中引用父类的基础方法(call(..)),代码是丑陋的
  • 在JavaScript中模拟类是得不偿失的

[[Prototype]]机制

  • 对象有一个特殊的内置属性[[Prototype]],其实就是对于其他对象的引用
  • 属性查找时都会查找[[Prototype]]链,直到找到属性或者查找完整条原型链
  • JavaScript是真正的“面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言。对象直接定义自己的行为。JavaScript中只有对象。
  • 所有函数默认都有一个名为prototype的公有并且不可枚举的属性,它指向一个对象,该对象也是在构造调用(new)时创建的那个对象的内部[[Prototype]]属性(即其原型)(新实例的原型链接)
function Foo() {
    // ...
}

var a = new Foo();

Object.getPrototypeOf(a) === Foo.prototype; // true getPrototypeOf返回对象的原型
a.__proto__ == Foo.prototype; // true 谷歌浏览器将[[Prototype]]实现为__proto__
  • 原型继承和类继承完全不同,继承意味着复制,而原型继承只是在对象之间创建关联,这样一个对象就可以通过委托访问另一个对象的属性和函数
  • Foo.prototype默认有一个公有并且不可枚举的属性constructor,这个属性引用的是对象关联的函数
  • new Foo()创建的对象本身并没有constructor属性,constructor引用被委托给了Foo.prototype,如果Foo.prototype对象也没有constructor属性,它会继续委托给委托链顶端的Object.prototype,这个对象的constructor属性指向内置的Object(..)函数
function Foo() {
    // ...
}

Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true 看作a对象由Foo构造是虚假的安全感
  • [[Get]]算法查找[[Prototype]]链。如果访问对象中并不存在的一个属性,[[Get]]操作就会查找对象内部[[Prototype]]关联的对象
  • a1.constructor是一个非常不可靠并且不安全的引用,通常来说要尽量避免使用这些引用
function Foo() { /* .. */ }

Foo.prototype = { /* .. */ };

Object.defineProperty(Foo.prototype, "constructor", {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo
});
  • 调用Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到指定的对象
/*两种把Bar.prototype关联到Foo.prototype的方法*/

// ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);

// ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
  • 检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省、反射
  • a instanceof Foo 回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象。这个方法只能处理对象和函数之间的关系
  • Foo.prototype.isPrototypeOf(a) 回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype
  • Object.getPrototypeOf(a) 直接获取一个对象的[[Prototype]]链
b.isPrototypeOf(a) // b是否出现在a的[[Prototype]]链中

Object.getPrototypeOf(a) === Foo.prototype; // true

a.__proto__ === Foo.prototype; // true
  • .__proto__和.toString()、.isPrototypeOf(..)等一样,存在于内置的Object.prototype中
  • [[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。这个链接的作用是:如果对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。这一系列对象的链接被称为“原型链”
  • 创建对象间的关联:Object.create(..)
  • Object.create(..)充分发挥[[Prototype]]机制的威力(委托)并且避免不要的麻烦(比如使用new的构造函数调用)
  • Object.create(null)会创建一个拥有空[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型,所以instanceof操作符无法进行判断,总是返回false
  • 空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
/*Object.create()的polyfill代码*/
if (! Object.create) {
    Object.create = function(o) {
        function F() {} // 一次性函数F
        F.prototype = o;
        return new F();
    };
}
  • 内部委托比直接委托可以让API接口设计更加清晰
  • 对象之间的关系不是复制而是委托

委托

  • 面向委托的设计模式,委托行为的设计模式,行为委托设计模式
  • 类设计模式,许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)
  • 委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)
Task = {
    setID: function(ID) { this.id = ID; },
    outputID: function(ID) { console.log(this.id); }
};

XYZ = Object.create(Task); // XYZ对象委托给Task

XYZ.prepareTask = function(ID, Label) {
    this.setID(ID);
    this.label = Label;
}

XYZ.outputTaskDetails = function() {
    this.outputID();
    console.log(this.label);
}
  • 这种编码风格称为“对象关联”(OLOO, objects linked to other objects)
  • 在委托行为中尽量避免在[[Prototype]]链的不同级别中使用相同的命名
  • 在[[Prototype]]委托中最好把状态保存在委托者(XYZ)而不是委托目标(Task)上
  • 对象并不是按照父类到子类的关系垂直组织的,而是通过任意方向的委托关联并排组织的
  • 在API接口的设计中,委托最好在内部实现,不要直接暴露出去
  • 禁止互相委托
  • 类风格代码vs.对象关联风格代码 父类子类关系vs.委托被委托关系(兄弟关系)
/*
面向对象风格
传统的原型语法
子类Bar继承了父类Foo,然后生成了b1和b2两个实例
b1委托了Bar.prototype, Bar.prototype委托了Foo.prototype
构造函数,原型以及new
*/
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();

/*
对象关联风格代码和行为委托设计模式
b1委托给Bar, Bar委托给Foo
*/
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();
  • 函数对象同样有[[Prototype]]属性并且关联到Function.prototype对象,函数对象都可以通过委托调用call(..)、apply(..)、bind(..)这些默认方法
  • class语法是ES6中的新语法糖,仍然是通过[[Prototype]]机制实现的,尽管语法上得到了改进,但实际上并没有真正的类。解决语法上的问题无法解除对于JavaScript中类的误解,尽管它看起来非常像一种解决方法
  • 委托设计模式没有像类一样在两个对象中都定义相同的方法名,而可以定义更具描述性的方法名
  • 通过对象关联避免丑陋的显式伪多态调用,避免使用构造函数、.prototype和new,不需要基类、实例化、合成
  • 对象关联可以更好地支持关注分离(separation of concerns)原则,创建和初始化并不需要合并为一个步骤
  • AuthController和LoginController都不具备对方的基础行为,所以继承关系是不恰当的,进行一些简单的合成从而让它们既不必互相继承又可以互相合作,和基类Controlller共三个实体
  • 反向委托也没有问题,只需要两个实体
  • 行为委托模式简化代码结构,简化整体设计
  • 对象关联风格的对象可以使用简洁方法(和class的语法糖一样),代码里不再有function了
/*ES6中可以在任意对象的字面形式中使用简洁方法声明(concise method declaration)*/
var LoginController = {
    errors: [],
    getUser() {..},
    getPassword() {..}
};
  • 简洁方法去掉语法糖后使用的是匿名函数表达式
  • 匿名函数表达式三大缺点:1. 调试栈更难追踪;2. 自我引用(递归、事件(解除)绑定等)更难;3. 代码(稍微更难理解)
  • 需要自我引用的话,最好使用传统的具名函数表达式来定义对应的函数(.baz: function baz(){..}),不要使用简洁方法
  • 内省就是检查实例的类型
  • instanceof 类风格 糟糕的方法 间接的形式Foo.prototype.isPrototypeOf(..)
  • 鸭子类型 脆弱的内省模式 具有某种方法便推测具有所有标准行为
  • 直接问“你是我的原型吗”
var Foo = { /* .. */ };

var Bar = Object.create(Foo);

var b1 = Object.create(Bar);

Foo.isPrototypeOf(Bar); // true
Object.getPrototypeOf(Bar) === Foo; //true

Foo.isPrototypeOf(b1); // true
Bar.isPrototypeOf(b1); // true
Object.getPrototypeOf(b1) === Bar; // true
  • JavaScript的[[Prototype]]机制本质上就是行为委托机制,我们可以选择努力实现类机制,也可以拥抱更自然的[[Prototype]]委托机制
  • class并不会像传统面向类的语言一样在声明时静态复制所有行为,只是使用基于[[Prototype]]的实时委托
  • ES6的class最大的问题在于,它的语法有时会让你认为,定义了一个class后,它就变成了一个未来会被实例化的东西的静态定义。你会忽略C是一个对象,是一个具体的可以直接交互的东西。JavaScript最强大的特性之一就是它的动态性,任何对象的定义都可以修改,class似乎不赞同这样做,所以强制让你使用丑陋的.prototype语法以及super问题。class似乎想告诉你:“动态太难实现了,所以这可能不是个好主意。这里有一种看起来像静态的语法,所以编写静态代码吧。”(但是实际上并不是)