Note On <You Don't Know JS - this and Object Prototypes>



Chapter 1

this is not an author-time binding but a runtime binding.

It is contextual based on the conditions of the function's invocation.

this binding has nothing to do with where a function is declared, but has instead everything to do with the manner in which the function is called.


When a function is invoked, an activation record, otherwise known as an execution context, is created. This record contains information about where the function was called from (the call-stack), how the function was invoked, what parameters were passed, etc. One of the properties of this record is the this reference, which will be used for the duration of that function's execution.


Chapter 2

How does the call-site determine where this will point during the execution of a function?


Rule 1: Default Binding

Standalone function invocation, this rule is the default when none of the other rules apply.

function foo() {
	console.log(this.a);
}

var a = 2;
foo(); // 2


It points this at the global object.


Note: If strict mode is in effect, the global object is not eligible for the default binding, so the this is instead set to undefined.
However, whether the global object is eligible for the default binding is determined only by if the contents of foo() are running in strict mode; not the state of the call-site of foo():
function foo() {
	console.log( this.a );
}

var a = 2;

(function(){
	"use strict";
	foo(); // 2
})();


Rule 2: Implicit Binding

In this case, the call-site has a context object.

function foo() {
	console.log(this.a);
}

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

obj.foo(); // 2


Regardless of whether foo() is initially declared on foo, or is added as a reference later (as this snippet shows), in neither case is the function really "owned" or "contained" by the obj object.


However, the call-site uses the obj context to reference the function, and when there is a context object for a function reference, the implicit binding rule says that it's that object that should be used for the function call's this binding.


Implicitly lost

Implicitly bound function can lose the binding, and it usually falls back to the default binding. It happens when passing a callback function as a parameter.

function foo() {
	console.log(this.a);
}

function doFoo(fn) {
	// `fn` is just another reference to `foo`
	fn(); // <-- call-site!
}

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

var a = "oops, global"; // `a` also property on global object
doFoo(obj.foo); // "oops, global"


Rule 3: Explicit Binding


function foo() {
	console.log(this.a);
}

var obj = {
	a: 2
};

foo.call(obj); // 2


Now you force its this to be obj.


Note: If you pass a simple primitive value (of type string, boolean, or number) as the this binding, the primitive value is wrapped in its object form (new String(..), new Boolean(..), or new Number(..), respectively). This is often referred to as "boxing."


Hard binding

But it doesn't solve implicitly lost. To fix it, you need Hard binding:

function foo() {
	console.log(this.a);
}

var obj = {
	a: 2
};

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

bar(); // 2
setTimeout(bar, 100); // 2
// hard-bound `bar` can no longer have its `this` overridden
bar.call(window); // 2


No matter how you later invoke the function bar, it will always manually invoke foo with obj. This binding is both explicit and strong, so we call it hard binding.

function foo(something) {
	console.log(this.a, something);
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = function() {
	return foo.apply( obj, arguments );
};

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


Or you can create a helper:

function foo(something) {
	console.log(this.a, something);
	return this.a + something;
}

// simple `bind` helper
function bind(fn, obj) {
	return function() {
		return fn.apply(obj, arguments);
	};
}

var obj = {
	a: 2
};

var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5


Actually it's provided with a builtin utility as of ES5, Function.prototype.bind, and it's used like 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


Rule 4: new Rule

In JS, constructors are just functions that happen to be called with the new operator in front of them. They are not attached to classes, nor are they instantiating a class. They are not even special types of functions. They're just regular functions.


Any ol' function, including the built-in object functions like Number(..) can be called with new in front of it, and that makes that function call a constructor call.


When a function is invoked with new in front of it, the following things are done automatically:

  1. A brand new object is created (aka constructed) out of thin air.
  2. The newly constructed object is [[Prototype]]-linked.
  3. The newly constructed object is set as the this binding for that function call.
  4. Unless the function returns its own alternate object, the new-invoked function call will automatically return the newly constructed object.


function foo(a) {
	this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2


Precedence of the Rules

  1. new binding
  2. explicit binding
  3. implicit binding
  4. default binding

new binding vs. explicit binding

function foo(something) {
    this.a = something;
}
var obj1 = {};

var bar = foo.bind(obj1);
bar(2);

console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3


new binding vs. implicit binding

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};

obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3

var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4


explicit binding vs. implicit binding

function foo() {
    console.log(this.a);
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2









Chapter 3: 对象

  • JS有一些內建的类型,比如基本类型的装裹类(wrapper class),像String,还有其他JS特有的类型,比如Function,Object,和一些复合类型如Array,和Date。作者提出一个有深度的观察,它们实际上不过是JS內建的构造器函数。
    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
    
    // inspect the object sub-type
    Object.prototype.toString.call( strObject );			// [object String]
  • 当在基本类型的变量上面调用方法或者访问属性时,其实它们会被自动转换为对应的装裹类的对象:
    var strPrimitive = "I am a string";
    
    console.log( strPrimitive.length );		// 13
    
    console.log( strPrimitive.charAt( 3 ) );	// "m"
  • 属性名只能是字符串:
    var myObject = { };
    
    myObject[true] = "foo";
    myObject[3] = "bar";
    myObject[myObject] = "baz";
    
    myObject["true"];		// "foo"
    myObject["3"];			// "bar"
    myObject["[object Object]"];	// "baz"
  • Array也是对象,所以你也可以给它添加属性,但是注意这个添加不同于在数组里添加元素,所以不会影响数组长度,还有就是不要用可以被转换为整型数字的字符串作为属性名。
  • 对象克隆:深拷贝vs浅拷贝。使用JSON
  • 用in来判断某个对象上面是否有某个属性,而不是验证该属性的值是否为undefined,因为,属性访问器[[GET]]的工作原理就是,找不到就返回undefined,所以如果一个属性的值也恰好是undefined,就会无法分辨。
  • ES5的属性描述器:


var myObject = {
	a: 2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
//    value: 2,
//    writable: true,
//    enumerable: true,
//    configurable: true
// }

var myObject = {};

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

myObject.a; // 2


下面的内容基本上都是在谈ES5里提供的新特性,就来一一验证下它们在最新版的Chrome和Firefox里的支持情况。我在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中试验了所有本章提到的ES5标准里的新语法。测试结果是两者的执行都和预期相同,这些代码如下:

var myObject = {
	a: 2
};

console.log(Object.getOwnPropertyDescriptor(myObject, "a"));
var myObject = {};

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

console.log(myObject.a); // 2
var myObject = {};

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, // not writable!
	configurable: true,
	enumerable: true
} );

myObject.a = 3;

console.log(myObject.a); // 2
"use strict";

var myObject = {};

Object.defineProperty( myObject, "a", {
	value: 2,
	writable: false, // not writable!
	configurable: true,
	enumerable: true
} );

myObject.a = 3; // TypeError
var myObject = {
	a: 2
};

myObject.a = 3;
console.log(myObject.a);					// 3

Object.defineProperty( myObject, "a", {
	value: 4,
	writable: true,
	configurable: false,	// not configurable!
	enumerable: true
} );

console.log(myObject.a);					// 4
myObject.a = 5;
console.log(myObject.a);					// 5

Object.defineProperty( myObject, "a", {
	value: 6,
	writable: true,
	configurable: true,
	enumerable: true
} ); // TypeError
var myObject = {
	a: 2
};

console.log(myObject.a);				// 2
delete myObject.a;
console.log(myObject.a);				// undefined

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

console.log(myObject.a);				// 2
delete myObject.a;
console.log(myObject.a);				// 2
var myObject = {
	a: 2
};

Object.preventExtensions( myObject );

myObject.b = 3;
console.log(myObject.b); // undefined
var myObject = { };

Object.defineProperty(
	myObject,
	"a",
	// make `a` enumerable, as normal
	{ enumerable: true, value: 2 }
);

Object.defineProperty(
	myObject,
	"b",
	// make `b` NON-enumerable
	{ enumerable: false, value: 3 }
);

console.log(myObject.b); // 3
console.log("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true

// .......

for (var k in myObject) {
	console.log( k, myObject[k] );
}
// "a" 2
var myObject = { };

Object.defineProperty(
	myObject,
	"a",
	// make `a` enumerable, as normal
	{ enumerable: true, value: 2 }
);

Object.defineProperty(
	myObject,
	"b",
	// make `b` non-enumerable
	{ enumerable: false, value: 3 }
);

console.log(myObject.propertyIsEnumerable( "a" )); // true
console.log(myObject.propertyIsEnumerable( "b" )); // false

console.log(Object.keys( myObject )); // ["a"]
console.log(Object.getOwnPropertyNames( myObject )); // ["a", "b"]
var myArray = [ 1, 2, 3 ];

for (var v of myArray) {
	console.log( v );
}
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();

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

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

// iterate `myObject` manually
var it = myObject[Symbol.iterator]();
console.log(it.next()); // { value:2, done:false }
console.log(it.next()); // { value:3, done:false }
console.log(it.next()); // { value:undefined, done:true }

// iterate `myObject` with `for..of`
for (var v of myObject) {
	console.log( v );
}
// 2
// 3
var randoms = {
	[Symbol.iterator]: function() {
		return {
			next: function() {
				return { value: Math.random() };
			}
		};
	}
};

var randoms_pool = [];
for (var n of randoms) {
	randoms_pool.push( n );

	// don't proceed unbounded!
	if (randoms_pool.length === 100) break;
}



Chapter 4: 混合对象



Chapter 5: 原型


[[prototype]]

首先是解释一下这个东西的概念,它是JS里对象的一个内部属性,在规范里叫[[Prototype]]。每个对象都有这个属性,一般来说它都会指向另外一个对象,也可以为null,这种情况不多见。而被指向的那个对象也是普通的对象,也有自己的[[Prototype]]属性,它会又指向另外一个对象,由此下去,就形成一个原型链条。


在对象上访问属性时,会隐性地触发调用内部方法[[Get]],它的执行过程的主要特点是,沿着这个[[Prototype]]构成的链条去寻找名字匹配的变量,如果最后都没有找到,就返回undefined。


值得注意的是,for..in和in操作也会沿着这个原型链查找,for..in只会罗列属性为enumerable的。


var anotherObject = {
	a: 2
};

// create an object linked to `anotherObject`
var myObject = Object.create( anotherObject );

for (var k in myObject) {
	console.log("found: " + k);
}
// found: a

console.log("a" in myObject); // true


在Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)中测试通过。


Object的源头

JS里有一个內建的Object对象(本人觉得本质讲它是个函数),它是一切新创建对象和函数默认的源头,新对象的[[prototype]]默认会指向它,新函数的prototype也会指向它,所以终归结底所有原型链的终点就是Object.prototype。它实现了.toString(),.valueOf()和.hasOwnProperty(..)等等方法。


设定属性,遮蔽属性(Shadowing Properties)

考虑下面这段代码:

myObject.foo = "bar";


这个操作背后发生的事情有几种情况:

  • myObject自己有foo属性,修改它;
  • myObject上面没有foo属性,它的原型链上也没有,添加foo属性到myObject;
  • myObject上面没有foo属性,但是它的原型链上有,那么情况变得有些微妙复杂,实际发生的事情也再分几种情况:
    • 原型链上的foo不是只读,那么在myObject上面添加foo;
    • 原型链上的foo是只读,那么整个操作失效,甚至在严谨模式下会抛出异常;
    • 如果foo是一个setter,那么调用执行该setter而已,不会发生其他事情。


在myObject自己有foo属性,并且它的原型链上也有的情况下,myObject自己的foo就会先返回,原型链上的就会被忽略,这种情况叫遮蔽,shadow


遮蔽或许是在程序员无意间导致的,像下面的情况:

var anotherObject = {
	a: 2
};

var myObject = Object.create(anotherObject);

console.log(anotherObject.a); // 2
console.log(myObject.a); // 2

console.log(anotherObject.hasOwnProperty("a")); // true
console.log(myObject.hasOwnProperty("a")); // false

myObject.a++; // oops, implicit shadowing!

console.log(anotherObject.a); // 2
console.log(myObject.a); // 3

console.log(myObject.hasOwnProperty("a")); // true


非要定义遮蔽属性的话,就用Object.defineProperty(..)。另外,对方法造成的遮蔽应该尽量被避免


“类”

这段基本上讲点哲学,作者挺愤慨地吐槽了下JS的一些做法。


JS没有类,就是对象,一切直接从对象开始。所以它更适合被称为面向对象的语言。第一个怪现象就是函数都有个prototype属性,而用new调用函数,会导致它的prototype属性被赋值给新对象的[[Prototype]]属性,作者认为这是个很误导人的现象。要创建对象,就直接用Object.create(..)就好了。


作者也不认同原型继承这个术语,甚至他觉得在JS里称“继承”都不是很妥当,继承隐含有拷贝的意思,而JS里根本没有拷贝发生。作者较为认同“差别化继承”这个术语,但是他又强调最终发生的事情也跟差别化没有多大关系,这些标签都只是一种在意念里概念化事物的方式。


接下来吐槽构造函数这个概念,其实没有构造函数一说,只有构造式调用函数(就是通过new),都是普通函数,全凭怎么调用而言。


作者又吐槽了函数上面的prototype.constructor这个方法,由于原型链查找,它会让经过函数创建的对象上面能访问到constructor这个属性,可是这个对象跟这个函数之间的关系却并没有那么牢固紧密,看下面代码:

function Foo() { /* .. */ }

Foo.prototype = { /* .. */ }; // create a new prototype object

var a1 = new Foo();
console.log(a1.constructor === Foo); // false!
console.log(a1.constructor === Object); // true!

要解决这种情况,你就得手动来添加一个constructor:

function Foo() { /* .. */ }

Foo.prototype = { /* .. */ }; // create a new prototype object

// Need to properly "fix" the missing `.constructor`
// property on the new object serving as `Foo.prototype`.
// See Chapter 3 for `defineProperty(..)`.
Object.defineProperty( Foo.prototype, "constructor" , {
	enumerable: false,
	writable: true,
	configurable: true,
	value: Foo    // point `.constructor` at `Foo`
} );

如果都有靠手动,那么又何必呢?


(原型)继承


这一段的多数内容在Dmitry的博客也有说。


典型的做法是下面这样:

function Foo(name) {
	this.name = name;
}

Foo.prototype.myName = function() {
	return this.name;
};

function Bar(name,label) {
	Foo.call( this, name );
	this.label = label;
}

// here, we make a new `Bar.prototype`
// linked to `Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );

// Beware! Now `Bar.prototype.constructor` is gone,
// and might need to be manually "fixed" if you're
// in the habit of relying on such properties!

Bar.prototype.myLabel = function() {
	return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码有个潜在的问题,就是Bar丢失了自己的constructor。


另外有一个常见的错误做法就是:

Bar.prototype = new Foo();


这个做法的问题在于,它在建立两个类的关系的过程里一定要调用执行Foo(),而如果Foo()里面有一些操作是影响对象状态的,它当然会在错误的对象上进行这些操作,这样就会有意料之外的结果发生,而且根据Dmitry的博客,如果在实例化Foo的时候Foo需要一些数据,那么在建立继承关系的时候,Foo是没法得到这些数据的。


所以作者推荐的做法,在ES6时代之前是:

Bar.prototype = Object.create(Foo.prototype);


在ES6时代则是:

Object.setPrototypeOf( Bar.prototype, Foo.prototype );


判断类之间的继承关系

继续吐槽,instanceof,这个操作符仅仅是检查前面的对象的原型链里有没有出现后面的函数,有的话就返回true。所以,这个操作符真正揭示的是函数与对象的关系,而不是对象与对象,所以它揭示的东西不可能是真正的在基于类的面向对象语言意义上的继承关系。


比较清晰的做法是:

Foo.prototype.isPrototypeOf( a );


它实际上验证的也是同样一个问题,Foo是否出现在a的原型链上,但重点是这个操作发生在两个对象之间。我设计了如下代码考察:

function Foo(name) {
	this.name = name;
}

function Bar(name,label) {
	Foo.call(this, name);
	this.label = label;
}

Bar.prototype = Object.create(Foo.prototype);

var a = new Bar("a", "obj a");

console.log(Foo.prototype.isPrototypeOf(a));	// true

var b = new Foo("vincent");

console.log(b.isPrototypeOf(a));	// false

在Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)中测试通过。


除了.isPrototypeOf()以外,ES5还提供了.getPrototypeOf()这样的方法,所以各位可以通过它们来在正式代码里访问对象的[[prototype]]了,这也是一直以来,很多浏览器支持的__proto__代表的东西。


前ES5时代

前面说了,Object.create()是ES5开始支持的,那么在之前的版本怎么做呢?

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


以这种方式可以给旧版JS打补丁,让它们支持Object.create()。这个方法也有另外一个功能,但是它无法被补丁到旧版JS里,因此也就比较少被使用:

var anotherObject = {
	a: 2
};

var myObject = Object.create( anotherObject, {
	b: {
		enumerable: false,
		writable: true,
		configurable: false,
		value: 3
	},
	c: {
		enumerable: true,
		writable: false,
		configurable: false,
		value: 4
	}
} );

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

myObject.a; // 2
myObject.b; // 3
myObject.c; // 4


有些人认为原型链就是一套“备用机制”,当当下的对象上寻找不到被请求的东西时,就退回到备用的对象上去寻找。对于这种理解原型的方式作者也不是很认可,但是作者觉得如果这种备用查找机制令代码的设计变得费解的话,可以通过使用代理模式来减少对代码设计的误读:

var anotherObject = {
	cool: function() {
		console.log( "cool!" );
	}
};

var myObject = Object.create( anotherObject );

myObject.doCool = function() {
	this.cool(); // internal delegation!
};

myObject.doCool(); // "cool!"


Chapter 6: 行为代理


面向代理的设计

作者很坚持正确的设计思路就是按照JS原本的设计原理去构建程序,而不是强行将一个基于原型的语言改变成基于类的语言,作者提出一种由原型代理的实现原理启发出的编程泛型,他称之为“对象链接至对象”(OLOO)和“面向代理”(Delegation-Oriented Design)。


作者举了很多例子,并对比使用这两种思维设计出的代码的不同。


第一个例子非常简单,只是演示目的的例子:

基于面向类的设计实现:

class Task {
	id;

	// constructor `Task()`
	Task(ID) { id = ID; }
	outputTask() { output( id ); }
}

class XYZ inherits Task {
	label;

	// constructor `XYZ()`
	XYZ(ID,Label) { super( ID ); label = Label; }
	outputTask() { super(); output( label ); }
}

class ABC inherits Task {
	// ...
}

基于OLOO的设计:

var Task = {
	setID: function(ID) { this.id = ID; },
	outputID: function() { console.log( this.id ); }
};

// make `XYZ` delegate to `Task`
var XYZ = Object.create( Task );

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

XYZ.outputTaskDetails = function() {
	this.outputID();
	console.log( this.label );
};

// ABC = Object.create( Task );
// ABC ... = ...


新设计的好处:

  • 两个属性,id和label都被定义在了XYZ上面,而不是代理对象Task;
  • 没有方法重载发生,两个对象上面没有相同名字的方法;
  • 在被代理对象XYZ上,调用setID()会由于原型链而执行代理对象Task的方法,但是this却是被绑定为XYZ自身。


另外,对象不可以同时相互代理,这样会造成循环。


这里有顺便讨论Chrome的调试器的一个问题。


接下来第二个例子:


面向类的:

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

基于OLOO的:

var Foo = {
	init: function(who) {
		this.me = who;
	},
	identify: function() {
		return "I am " + this.me;
	}
};

var 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();



类vs.对象

一个小插件的例子:


基于面向类的实现:

// Parent class
function Widget(width,height) {
	this.width = width || 50;
	this.height = height || 50;
	this.$elem = null;
}

Widget.prototype.render = function($where){
	if (this.$elem) {
		this.$elem.css( {
			width: this.width + "px",
			height: this.height + "px"
		} ).appendTo( $where );
	}
};

// Child class
function Button(width,height,label) {
	// "super" constructor call
	Widget.call( this, width, height );
	this.label = label || "Default";

	this.$elem = $( "<button>" ).text( this.label );
}

// make `Button` "inherit" from `Widget`
Button.prototype = Object.create( Widget.prototype );

// override base "inherited" `render(..)`
Button.prototype.render = function($where) {
	// "super" call
	Widget.prototype.render.call( this, $where );
	this.$elem.click( this.onClick.bind( this ) );
};

Button.prototype.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" );
};

$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = new Button( 125, 30, "Hello" );
	var btn2 = new Button( 150, 40, "World" );

	btn1.render( $body );
	btn2.render( $body );
} );

基于OLOO的实现:

var Widget = {
	init: function(width,height){
		this.width = width || 50;
		this.height = height || 50;
		this.$elem = null;
	},
	insert: function($where){
		if (this.$elem) {
			this.$elem.css( {
				width: this.width + "px",
				height: this.height + "px"
			} ).appendTo( $where );
		}
	}
};

var Button = Object.create( Widget );

Button.setup = function(width,height,label){
	// delegated call
	this.init( width, height );
	this.label = label || "Default";

	this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
	// delegated call
	this.insert( $where );
	this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" );
};

$( document ).ready( function(){
	var $body = $( document.body );

	var btn1 = Object.create( Button );
	btn1.setup( 125, 30, "Hello" );

	var btn2 = Object.create( Button );
	btn2.setup( 150, 40, "World" );

	btn1.build( $body );
	btn2.build( $body );
} );

使用ES6的语法的实现:

class Widget {
	constructor(width,height) {
		this.width = width || 50;
		this.height = height || 50;
		this.$elem = null;
	}
	render($where){
		if (this.$elem) {
			this.$elem.css( {
				width: this.width + "px",
				height: this.height + "px"
			} ).appendTo( $where );
		}
	}
}

class Button extends Widget {
	constructor(width,height,label) {
		super( width, height );
		this.label = label || "Default";
		this.$elem = $( "<button>" ).text( this.label );
	}
	render($where) {
		super.render( $where );
		this.$elem.click( this.onClick.bind( this ) );
	}
	onClick(evt) {
		console.log( "Button '" + this.label + "' clicked!" );
	}
}

$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = new Button( 125, 30, "Hello" );
	var btn2 = new Button( 150, 40, "World" );

	btn1.render( $body );
	btn2.render( $body );
} );


结论:ES6的方法和面向类的方法都不推荐使用。不推荐ES6的原因是,它归根结底也只是在语法层面加了些补丁,而底层的实现依然是基于原型的,所以反而容易造成误解。


更简化的设计

接下来是更复杂的例子:


面向类的:

// Parent class
function Controller() {
	this.errors = [];
}
Controller.prototype.showDialog = function(title,msg) {
	// display title & message to user in dialog
};
Controller.prototype.success = function(msg) {
	this.showDialog( "Success", msg );
};
Controller.prototype.failure = function(err) {
	this.errors.push( err );
	this.showDialog( "Error", err );
};

// Child class
function LoginController() {
	Controller.call( this );
}
// Link child class to parent
LoginController.prototype = Object.create( Controller.prototype );
LoginController.prototype.getUser = function() {
	return document.getElementById( "login_username" ).value;
};
LoginController.prototype.getPassword = function() {
	return document.getElementById( "login_password" ).value;
};
LoginController.prototype.validateEntry = function(user,pw) {
	user = user || this.getUser();
	pw = pw || this.getPassword();

	if (!(user && pw)) {
		return this.failure( "Please enter a username & password!" );
	}
	else if (pw.length < 5) {
		return this.failure( "Password must be 5+ characters!" );
	}

	// got here? validated!
	return true;
};
// Override to extend base `failure()`
LoginController.prototype.failure = function(err) {
	// "super" call
	Controller.prototype.failure.call( this, "Login invalid: " + err );
};

// Child class
function AuthController(login) {
	Controller.call( this );
	// in addition to inheritance, we also need composition
	this.login = login;
}
// Link child class to parent
AuthController.prototype = Object.create( Controller.prototype );
AuthController.prototype.server = function(url,data) {
	return $.ajax( {
		url: url,
		data: data
	} );
};
AuthController.prototype.checkAuth = function() {
	var user = this.login.getUser();
	var pw = this.login.getPassword();

	if (this.login.validateEntry( user, pw )) {
		this.server( "/check-auth",{
			user: user,
			pw: pw
		} )
		.then( this.success.bind( this ) )
		.fail( this.failure.bind( this ) );
	}
};
// Override to extend base `success()`
AuthController.prototype.success = function() {
	// "super" call
	Controller.prototype.success.call( this, "Authenticated!" );
};
// Override to extend base `failure()`
AuthController.prototype.failure = function(err) {
	// "super" call
	Controller.prototype.failure.call( this, "Auth Failed: " + err );
};

var auth = new AuthController(
	// in addition to inheritance, we also need composition
	new LoginController()
);
auth.checkAuth();

使用OLOO的:

var LoginController = {
	errors: [],
	getUser: function() {
		return document.getElementById( "login_username" ).value;
	},
	getPassword: function() {
		return document.getElementById( "login_password" ).value;
	},
	validateEntry: function(user,pw) {
		user = user || this.getUser();
		pw = pw || this.getPassword();

		if (!(user && pw)) {
			return this.failure( "Please enter a username & password!" );
		}
		else if (pw.length < 5) {
			return this.failure( "Password must be 5+ characters!" );
		}

		// got here? validated!
		return true;
	},
	showDialog: function(title,msg) {
		// display success message to user in dialog
	},
	failure: function(err) {
		this.errors.push( err );
		this.showDialog( "Error", "Login invalid: " + err );
	}
};
// Link `AuthController` to delegate to `LoginController`
var AuthController = Object.create( LoginController );

AuthController.errors = [];
AuthController.checkAuth = function() {
	var user = this.getUser();
	var pw = this.getPassword();

	if (this.validateEntry( user, pw )) {
		this.server( "/check-auth",{
			user: user,
			pw: pw
		} )
		.then( this.accepted.bind( this ) )
		.fail( this.rejected.bind( this ) );
	}
};
AuthController.server = function(url,data) {
	return $.ajax( {
		url: url,
		data: data
	} );
};
AuthController.accepted = function() {
	this.showDialog( "Success", "Authenticated!" )
};
AuthController.rejected = function(err) {
	this.failure( "Auth Failed: " + err );
};


新的设计不只是简化了代码,也简化了关系,而且只有两个类,类之间的合成关系也可以简单地用代理来实现。


更漂亮的语法

ES6的新语法允许程序员这样写:

class Foo {
	methodName() { /* .. */ }
}


声明对象的语法也相对简化:

var LoginController = {
	errors: [],
	getUser() { // Look ma, no `function`!
		// ...
	},
	getPassword() {
		// ...
	}
	// ...
};



但是新语法也可能造成些不便,比如下面写法:

var Foo = {
	bar() { /*..*/ },
	baz: function baz() { /*..*/ }
};

bar()将无法用自己的名字直接递归调用自己,而只能通过Foo.bar()。


内省

再次强调,instanceof所操作的是一个对象和一个函数,但是通过它想要反应的却是所谓类和其实例的关系,而实际上,它运作的方式是判断一个函数的.prototype是否出现在该对象的原型链上面,这种做法其实很间接,所以不可靠,另外也非常误导人。


用isPrototypeOf()和getPrototypeOf()就好很多。


附录


说了这么多,先来试下ES6的新语法的支持情况吧。根据作者的代码,我整理了如下测试代码:

class Task {
	constructor(ID) {
		this.id = ID || 0;
	}

	outputTask() { console.log(this.id); }
}

class XYZ extends Task {
	constructor(ID,Label) {
		super(ID);
		this.label = Label || "Default";
	}

	outputTask() { super.outputTask(); console.log( this.label ); }
	
	}
	
var x = new XYZ(2, "Roberto");
x.outputTask();


在在Chrome Version 57.0.2987.133 (64-bit)和Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)中测试通过。


下面作者整理了下这个新语法的缺陷。

  • 首先,不能定义成员变量,只能定义方法,于是如果要定义变量,还是得使用.prototype,这就违背了新语法的意图:
    class C {
    	constructor() {
    		// make sure to modify the shared state,
    		// not set a shadowed property on the
    		// instances!
    		C.prototype.count++;
    
    		// here, `this.count` works as expected
    		// via delegation
    		console.log( "Hello: " + this.count );
    	}
    }
    
    // add a property for shared state directly to
    // prototype object
    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
  • 另外,意外的遮蔽还是可能会发生,比如在使用this.c++这样操作的时候。
    class C {
    	constructor(id) {
    		// oops, gotcha, we're shadowing `id()` method
    		// with a property value on the instance
    		this.id = id;
    	}
    	id() {
    		console.log( "Id: " + this.id );
    	}
    }
    
    var c1 = new C( "c1" );
    c1.id(); // TypeError -- `c1.id` is now the string "c1"
  • 再有,super()是静态绑定的,不像this
    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
    };
    
    // Link E to D for delegation
    Object.setPrototypeOf( E, D );
    
    E.foo(); // "P.foo"
    这个问题是可以用toMethod()解决的,但是示例代码好像过于简单,我没有看出它是怎么解决问题的
    var D = {
    	foo: function() { console.log( "D.foo" ); }
    };
    
    // Link E to D for delegation
    var E = Object.create( D );
    
    // manually bind `foo`s `[[HomeObject]]` as
    // `E`, and `E.[[Prototype]]` is `D`, so thus
    // `super()` is `D.foo()`
    E.foo = C.prototype.foo.toMethod( E, "foo" );
    
    E.foo(); // "D.foo"


ES6新增加的这些貌似面向类的语法其实只是徒有其表,其底层机制依然没有改变,依然是原型,所以作者的态度是不用也罢,还是可以选择OLOO的方式,下面的代码演示了底层实现依然是靠原型:

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" -- oops!!!


最后的最后,作者依然是回归吐槽ES6的这套新语法,作者的解读是,这种语法暗示着程序员这样写出来的类是静态的定义,而尤其产生的对象也是静态不变的,但是实际上你还是可以在创建之后动态地对它们进行改变,甚至通过.prototype改变继承的所属关系添加新方法等等,可是这种新的语法的暗示表示它不建议程序员使用JS的这种动态的特性。好像它是在劝程序员:动态的东西太危险了,那么咱们假装咱们是静态的吧。


注:通过.bind(..)创建的硬绑定函数是不能再通过extends继承的




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值