class Parent {
constructor() {
if (new.target === Parent) {
console.log( "Parent instantiated" );
}
else {
console.log( "A child instantiated" );
}
}
}
class Child extends Parent {}
var a = new Parent();
// Parent instantiated
var b = new Child();
// A child instantiated
var arr = [4,5,6,7,8,9];
for (var v of arr) {
console.log( v );
}
// 4 5 6 7 8 9
// define iterator that only produces values
// from odd indexes
arr[Symbol.iterator] = function*() {
var idx = 1;
do {
yield this[idx];
} while ((idx += 2) < this.length);
};
for (var v of arr) {
console.log( v );
}
// 5 7 9
Symbol.toStringTag和Symbol.hasInstance
元编程中最常见的任务就是去内省一个值,判断一个类型,以此决定其业务逻辑。最常见的两种检验技术就是toString()和instance of
比如:
function Foo() {}
var a = new Foo();
a.toString(); // [object Object]
a instanceof Foo; // true
从 ES6 开始,你能够控制toString()和instance of的行为:
function Foo(greeting) {
this.greeting = greeting;
}
Foo.prototype[Symbol.toStringTag] = "Foo";
Object.defineProperty( Foo, Symbol.hasInstance, {
value: function(inst) {
return inst.greeting == "hello";
}
} );
var a = new Foo( "hello" ),
b = new Foo( "world" );
b[Symbol.toStringTag] = "cool";
a.toString(); // [object Foo]
String( b ); // [object cool]
a instanceof Foo; // true
b instanceof Foo; // false
class Cool {
// Symbol.species属性默认的读取器
static get [Symbol.species]() { return this; }
again() {
return new this.constructor[Symbol.species]();
}
}
class Fun extends Cool {}
class Awesome extends Cool {
// 强制将子类的构造行为变成父类的
static get [Symbol.species]() { return Cool; }
}
var a = new Fun(),
b = new Awesome(),
c = a.again(),
d = b.again();
c instanceof Fun; // true
d instanceof Awesome; // false
d instanceof Cool; // true
var o = { a:1, b:2, c:3 },
a = 10, b = 20, c = 30;
o[Symbol.unscopables] = {
a: false,
b: true,
c: false
};
with (o) {
console.log( a, b, c ); // 1 20 3
}
var o = {};
o[Symbol("c")] = "yay";
o[2] = true;
o[1] = true;
o.b = "awesome";
o.a = "cool";
Reflect.ownKeys( o ); // [1,2,"b","a",Symbol(c)]
Object.getOwnPropertyNames( o ); // [1,2,"b","a"]
Object.getOwnPropertySymbols( o ); // [Symbol(c)]
如果还有原型上的键值呢?
var o = { a: 1, b: 2 };
var p = Object.create( o );
p.c = 3;
p.d = 4;
for (var prop of Reflect.enumerate( p )) {
console.log( prop );
}
// c d a b
for (var prop in p) {
console.log( prop );
}
// c d a b
JSON.stringify( p );
// {"c":3,"d":4}
Object.keys( p );
// ["c","d"]
Javascript 元编程
原文链接:https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20&%20beyond/ch7.md
参考链接:http://es6.ruanyifeng.com/#docs/symbol
元编程是一种操作程序本身的编程方式。换句话来说,就是编写操作代码的代码,听起来是不是很酷?
举个例子,如果你想去探测一个对象
a
和另一个对象b
他们的是不是在一条原型链上?可以用a.isPrototypeOf(b)
,这通常为认为是一种元编程的方式—自省。宏(Javascript 还没有宏)是另外一种在那些编译型语言中的元编程的方式。用for..in
来迭代遍历一个对象的key
,或者检查一个对象是不是一个class constructor
的实例,这些都是另外一些元编程需要做的事情。元编程关注以下这些规定:
元编程的目标是利用语言自己本身的能力更好的让你的代码变得更加的可描述,有表现力和灵活。正是因为元编程的元性质,所以找一个更精确的定义给它是一件比较难的事情。理解元编程的最好的方法,我觉得就是通过一个个例子。
ES6
为 Javascript 在原有的基础之上增加了一些新的特性来更好的支持元编程。函数名
在有的时候,你的代码也许想要检查自己并想知道一些函数的名字是什么。如果你询问一个函数的名字是什么,答案会是出奇的模糊的。我们细想一下:
在上面的代码段中,想想看这么几个问题?
obj.foo()
的名字是什么?是"foo"
,""
,underfined
?obj.bar()
的名字是什么?是"bar"
或者"baz"
?obj.bam()
的名字是什么?是"bam"
或者"daz"
?obj.zim()
的名字是什么?除此之外,还有那些作为回调函数传递的函数呢?
在程序中,我们有相当多的方法让那些函数名模糊的函数变得具有可描述性。
还有更重要的是,我们需要区分函数的名字(
"name" of a function
)是不是来自于函数自己的"name"
属性,或者说它来自于它的词法约束名,比如"bar"
和function bar() {}
的关系?答案是是的。函数的词法约束名是什么?在递归中,你应该用过:
name
属性是我们用元编程的关键之处,所以我们会集中在这上面讨论。关于函数名的混淆问题是因为在默认情况下,函数的词法绑定名和函数的
name
属性是一样的。实际上在ES5
(包括之前的版本)对其这样的行为都没有官方的需求和解释。name
的属性设置是非标准的,但确实有需求,并可靠的。值得庆幸的是,从ES6
开始,name
属性已经变得是标准化了。推断
但是一个没有词法绑定名的函数它的
name
属性是什么呢?从
ES6
开始有了标准的推断规则来确定一个合理的name
属性的值来分配给一个函数,及时函数没有被赋予词法绑定名。如果我们强制给定词法绑定名的话,像这样
那么
name
属性的值将会理所当然的是"def"
。在没有缺少词法绑定名的情况下,直观的"abc"
看起来才是适当的name
的值。下面是在
ES6
中的其他形式的声明的函数推断出来name
属性的值的结果:name
属性是默认不可写的,但是是可配置的,这意味着如果你十分想重写它的话,可以用Object.defineProperty()
来实现。元属性
在本书在三章的
new.target
部分,我们引入了ES6
一个新概念:元属性。元属性旨在提供特殊的元信息的访问方式。在有
new.target
的情况下,关键字new
被用作为一个属性访问的上下文。准确的来说,new
自己本身并不是一个对象,只是它有特殊的能力而已(使得new
看起来像一个对象了)。不过,在函数被构造调用(一个函数或者方法被new
调用)的时候的内部用了new.target
,new
变成了一个虚拟的上下文,因此那个new.target
能够指向被new
调用的构造函数。一个很清楚的元编程操作的,其目的是从构造函数确定
new
的目标是什么,内省(检查输入/结构)或者静态属性的访问。举个例子,如果你想要根据直接调用或者被一个子类调用产生不同的行为的话,可以这样做:
这里有一些细小的差别,在父类中的构造函数是被实际赋予了词法绑定名的。
需要注意的是:如同所有的元编程技巧,要小心创建你未来的自己或他人维护你的代码,让其是能够被理解的,谨慎使用这些编程方法。
Symbols
在第二章的
"Symbols"
部分中,我们了解了ES6
中新的原始类型symbol
,除此之外,你可以在自己的程序中定义symbols
,Javascript 预定义了些内置的symbols
。这些内置的
symbols
主要是为了暴露一些特殊的元属性来让你对 Javascript 的行为有更多的控制权。我们来简单介绍和讨论下它们的用处。
Symbol.iterator
在第二章和第三种中,我们已经介绍并使用过了
@@iterator
,它会在展开运算符...
和for..of
循环中被自动使用。同时我们也看到了@@iterator
也被 ES6 作为一个新的部分所添加。Symbol.iterator
指向该对象的默认遍历器方法,即该对象进行for..of
循环时,会调用这个方法。然而,我们还可以通过
Symbol.iterator
为任何对象定义自己想要的的迭代逻辑,甚至去重写默认的迭代器。元编程就是我们去定义 Javascript 的那些操作运算循环结构的行为。看下这个例子:
Symbol.toStringTag
和Symbol.hasInstance
元编程中最常见的任务就是去内省一个值,判断一个类型,以此决定其业务逻辑。最常见的两种检验技术就是
toString()
和instance of
比如:
从 ES6 开始,你能够控制
toString()
和instance of
的行为:对象的
Symbol.toStringTag
属性,指向一个方法。在该对象上面调用Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。对象的
Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用instance of
运算符,判断是否为该对象的实例时,会调用这个方法。Symbol.species
在第三章中,我们介绍过了
@@species
,它指向一个方法,该对象作为构造函数创造实例时,会调用这个方法。我们可以通过
Symbol.species
来改写构造函数默认行为:如果
this.constructor[Symbol.species]
存在,就会使用这个属性作为构造函数,来创造新的实例对象。如果你想定义一个方法生成一个新的实例,你应该使用
new this.constructor[Symbol.species](..)
,而不是new this.constructor(..)
或者new XYZ(..)
,这样子类是能通过Symbol.species
来控制构造函数能生产你想要你的实例。Symbol.toPrimitive
在类型和语法系列中,我们讨论过一些类型的显式隐式转换,在 ES6 之前,我们是没有办法去控制变量类型转换的行为的。
从 ES6 开始,对象的
Symbol.toPrimitive
属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。从此我们可以定义自己的类型转换的行为。Symbol.toPrimitive
被调用时,会接受一个字符串hint
参数,表示当前运算的模式,一共有三种模式。Number
:该场合需要转成数值String
:该场合需要转成字符串Default
:该场合可以转成数值,也可以转成字符串上面的例子想告诉我们,
+
运算符是default
模式,*
运算符是数值模式
,String(..)
是字符串模式。注意:
==
运算符是default
模式,===
运算符不会命中任何模式。Regular Expression Symbols
下面是4个关于正则表达式对象的内置
Symbols
,它们能够控制字符串原型上的4个同名方法:对象的
Symbol.match
属性,指向一个函数。当执行str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值。对象的
Symbol.replace
属性,指向一个方法,当该对象被String.prototype.replace
方法调用时,会返回该方法的返回值。对象的
Symbol.search
属性,指向一个方法,当该对象被String.prototype.search
方法调用时,会返回该方法的返回值。对象的
Symbol.split
属性,指向一个方法,当该对象被String.prototype.split
方法调用时,会返回该方法的返回值。注意:覆盖内置的正则表达式算法,你需要十分的小心,因为 Javascript 自己的正则表达式引擎已经是高度优化过了,如果你使用自己的代码,可能会慢很多。元编程虽然强大,但是你需要在真的有必要的情况和油漆的情况下使用。
Symbol.isConcatSpreadable
对象的
Symbol.isConcatSpreadable
属性等于一个布尔值,表示该对象使用Array.prototype.concat()
时,是否可以展开。Symbol.unscopables
对象的
Symbol.unscopables
属性,指向一个对象。该对象指定了使用with
关键字时,哪些属性会被with
环境排除。true
意味着对象上的这个属性会被with
排除,因此从词法返回变量中过滤掉,由with
环境外的值代替。false
则相反。注意:在 Javascript 的
strict
模式下,with
是被禁用的。因此我们考虑不要用它。在作用域和闭包章节,我们知道了,应该避免去使用with
,所以Symbol.unscopables
也就变得无意义了。Proxies
在
ES6
中一个最明显的为元编程提供的特性就是Proxy
了。Proxy
是为其他的一些普通的对象最一层拦截,你能够其上注册一些特殊的回调,外界对该对象的访问,都会通过这层拦截,并触发回调。因此提供了一种机制,可以对外界的访问进行过滤和改写,为目标对象添加自己额外的逻辑。下面这个例子,就是在对象的一个属性的
get
进行拦截:我们声明
handlers
,并给它定义了一个get
方法(其实handlers
就是Proxy()
的第二个参数),它接受到目标对象obj
,关键属性a
和代理对象pobj
。通过
Reflect.get()
,我们不仅执行了console.log()
,还进入了obj
中,对属性a
就行了访问。我们将在下介绍Reflect
,但是请注意,每一个可用的代理都会有一个对应的同名的反射函数。这些同名的设计的映射是故意而为之的,执行一个相应的元编程的时候,代理对其进行拦截,同时反射在每个对象上执行元编程任务。每个代理都有其默认的定义,对应的反射会被自动调用。在绝大多数情况下,你是会同时用到代理和反射的。
下面是
Proxy
支持的拦截操作一览。对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
get(target, propKey, receiver)
拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
、Reflect.get(..)
,返回类型不限。最后一个参数receiver
可选,当target
对象设置了propKey
属性的get
函数时,receiver
对象会绑定get
函数的this
对象。set(target, propKey, value, receiver)
拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
、Reflect.set(..)
,返回一个布尔值。has(target, propKey)
拦截
propKey in proxy
、Reflect.has(..)
的操作,返回一个布尔值。deleteProperty(target, propKey)
拦截
delete proxy[propKey]
、Reflect.deleteProperty(..)
的操作,返回一个布尔值。enumerate(target)
拦截
for (var x in proxy)
、Reflect.enumerate(..)
,返回一个遍历器。ownKeys(target)
拦
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、Reflect.get()
、Reflect.ownKeys(..)
,返回一个数组。该方法返回对象所有自身的属性,而Object.keys()
仅返回对象可遍历的属性。getOwnPropertyDescriptor(target, propKey)
拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
、Reflect.getOwnPropertyDescriptor(..)
,返回属性的描述对象。defineProperty(target, propKey, propDesc)
拦截
Object.defineProperty(proxy, propKey, propDesc
)、Object.defineProperties(proxy, propDescs)
、Reflect.defineProperty(..)
,返回一个布尔值。preventExtensions(target)
拦截
Object.preventExtensions(proxy)
、Reflect.preventExtensions(..)
,返回一个布尔值。getPrototypeOf(target)
拦
Object.getPrototypeOf(proxy)
、Reflect.preventExtensions(..)
、__proto__
、Object.isPrototypeOf()
、instanceof
,返回一个对象。isExtensible(target)
拦截
Object.isExtensible(proxy)
、Reflect.isExtensible(..)
,返回一个布尔值。setPrototypeOf(target, proto)
拦截
Object.setPrototypeOf(proxy, proto)
、Reflect. setPrototypeOf(..)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args)
拦截
Proxy
实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
、Reflect.apply(..)
。construct(target, args, proxy)
拦截Proxy实例作为构造函数调用的操作,比如
new proxy(...args)
、Reflect.construct(..)
。注意:需要更多以上的相关信息,需要你看接下来的
Reflect
章节。除了以上列表给出的触发动作以外,还有些拦截可能是通过其他其他动作间接触发。比如:
getOwnPropertyDescriptor(..)
和defineProperty(..)
被默认的set()
触发了,如果你定义了自己set()
的拦截,你可能会也可能不会使用上下文中的变量,这将触发这些代理(自己定义set()
后,不触发了。)Proxy 的局限
有些操作是不属于可拦截的。比如,对象的运算操作是不能被拦截的:
也许在未来,Javascript 能给我们这样的权利。
使用
Proxy
代理的前置与后置
这些代理处理程序的元编程的好处应该是显而易见的。几乎完全拦截我们可以(和覆盖)对象的行为,这意味着我们可以在一些非常强大的方式超出核心 Javascript 对象行为。我们来看看几个例子模式探索的可能性。
正如我们前面所提到的,通常认为代理就是“包装”的目标对象。在这个意义上,代理成为主对象的代码的接口,为实际的目标对象保持隐藏/保护。
有时参数接受变量,不一定是值得信赖,所以你需要强制执行一些特殊的规则。比如:
我称这种设计为代理前置,这也是主要的代理设计。
我们强制定义了一些特殊的规则去影响
messages_proxy
,具体逻辑请阅读上面例子的注释。或者,我们可以完全反转这个模式。当目标对象影响代理而不是代理影响目标对象的时候,代码真的只与主对象互相影响了。做到这一点最简单的方案是要在主要对象的
[[Prototype]]
链中代理对象。比如:
我们直接和
greeter
而不是和catchall
交互。当我们调用speak()
,它是在greeter
上发现被直接调用的。但是,当我们试图去调用像everyone()
的方法的时候,改方法其实是不存在的。默认的对象属性会去查找
[[Prototype]]
链,所以catchall
会去参看对象上的每一个属性。代理的get()
会去调用speak()
,所以才有hello everyone!
这一结果。我称这种设计为代理后置,这是使用代理的最后手段。
没有找到这样的属性或者方法
关于 Javascipt 常见的抱怨是:设置不存在的属性的时候是没有报错的。你可能希望预定义所有的对象属性和方法,并且如果有一个不存在的属性名随后使用抛出一个错误。
我们可以通过使用
Proxy
来实现,代理的前置后置都可以,比如:通过
get()
和set()
的代理,我们可以在真正的操作之前去判断对象的属性是否存在,如果是不存在的属性,就抛出错误。代理对象pobj
是主要的与代码交互的对象,它为真是的对象提供了保护。现在,我们看下代理后置:
代理后置很简单,我们不需要去拦截
[[Get]]
和[[Set]]
,只要去期待他们,如果目标属性存在,我们就根据默认行为去访问它,如果不存在,就到了原型链上去找对应的属性,结果就被拦截了,抛出了错误。在代码里面,我们少些了很多的逻辑,很酷吧?让代理去“改变”原型链
原型链的向上查找机制是我们所熟知的。在对象如果一个属性不存在,则会向其原型继续查找,这意味着你可以使用代理的
get()
去模拟或者拓展原型机制的概念。我们第一个要改变的事情是去考虑创建两个通过原型循环链接的对象(或者,至少它们看起来是这样的。)你不能真正创造一个真正的双向循环的原型链,因为 Javascript 的引擎会抛出错误。但是
Proxy
可以伪造!注意:在这个例子中,我们不需要使用
set()
代理,我们想让例子变得简单。如果为了完全模仿原型链机制,你可能还要去实现一个set()
,搜索原型链相匹配的属性并遵守对象属性的描述法行为。在上面的代码片段中,
obj2
是通过以obj1
为原型创建的。但要创建反向的联动的,我们就需要在obj1
的属性上创建一个Symbol.for("[[Prototype]]")
,这个symbol
的设置看似十分的黑魔法,但事实并非如此,它只是让程序能方便的和要执行的任务所关联。然后,代理的
get()
首先去查找,先判断目标对象上有没有该key
,如果没有,就手动切换到存储在Symbol.for("[[Prototype]]")
,接下来的任务就交给Symbol.for("[[Prototype]]")
的值了。这种模式的一个重要的优点是,
obj1
和obj2
的定义基本是不通过它们之间的这种循环关系侵入。虽然上面的代码为了说明白道理而有些琐碎,但如果你仔细观察,代理的处理程序逻辑是通用的(并不需要明确的obj1
和obj2
)。所以这些逻辑完全可以抽象出来,写成一个可以叫setCircularPrototypeOf(..)
的函数,我们把这个函数的完成读者。现在,我们已经看到了如何通过代理
get()
去模拟原型链,让我们去想的更加远一点,那一个对象有多个原型(又名“多重继承”)?实现起来相当简单:注意:和前面在实现循环原型链中提到的一样,我们没有去实现
set()
相关的处理,模拟[[Set]]
的动作会很复杂。obj3
被设置看似是2个原型。在obj3.baz()
中,会去先去请求obj1.foo()
(因为先到先得,我们obj3[ Symbol.for("[[Prototype]]" )]
数组中的第一个值是obj1
)。如果我们改变顺序,让obj2
排在第一个,可能结果就不一样了。但是,如果在
obj1
中,没有找到bar()
,它就会去obj2
去查找了。obj1
和obj2
代表了obj3
的两个平行的原型链。obj1
和obj2
本身可能还有自己正常的原型,也可以像obj3
那样是模拟的原型,可多委托。正如前面的循环原型链一样,多重继承一样可以抽象出一个可以叫
setPrototypesOf(..)
的函数(注意这里有个“S”),这个任务同样留个读者吧!使用
Reflect
Reflect
对象和是一个普通的对象(像Math
),而不是一个内置函数或者构造器。它拥有对应的可以控制各种元编程任务的静态方法。这些功能和
Proxy
一一对应。下面的这些名称你可能看起来很眼熟(因为他们也是
Object
上的方法):Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
这些方法和在
Object
上的同名方法一样。然后,一个区别在于,Object
上这么方法的第一个参数是一个对象,Reflect
遇到这种情况会扔出一个错误。补充:
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect
对象的设计目的有这样几个。Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。一个对象的键们可以这样被访问:
Reflect.ownKeys(..)
:返回对象自己的键(不是通过“继承”的),同样也返回Object.getOwnPropertyNames()
和Object.getOwnPropertySymbols()
Reflect.enumerate(..)
:返回对象上可枚举的属性(包括“继承”过来的)。Reflect.has(..)
:和in
操作符差不多。函数的调用和构造调用可以手动通过下面 API 执行:
-
Reflect.apply(..)
:举个例子,Reflect.apply(foo,thisObj,[42,"bar"])
调用了foo()
函数,上下文是thisObj
,参数为42
和bar
。-
Reflect.construct(..)
:举个例子,Reflect.construct(foo,[42,"bar"])
等于new foo(42,"bar")
。对象属性访问,设置,删除可以手动通过下面 API 执行:
Reflect.get()
:Reflect.get(o,"foo")
等于o.foo
。Reflect.set()
:Reflect.set(o,"foo",42)
等于o.foo = 42
。Reflect.deleteProperty()
:Reflect.deleteProperty(o,"foo")
等于delete o.foo
。Reflect
的元编程能力可以让你等效的模拟以前隐藏的各种语法特性。这样你就可以使用这些功能为特定语言(DSL)拓展新功能和 API。在 ES6 之前,规范里面没有定义如何列出顺序的列出对象和列出对象的性能,一般来说,是大多数 Javascript 的引擎来创建对象属性的属性的,因此软件开发者们强烈建议不用根据依赖它排的顺序。
到了 ES6,对象的可枚举的键的顺序的算法被规定。
这个规则是:
String
创建排序。Symbol
创建顺序。例子:
如果还有原型上的键值呢?
在 ES6 中,
Reflect.ownKeys(..)
,Object.getOwnPropertyNames(..)
,Object.getOwnPropertySymbols(..)
的顺序是可靠可预测的,因此它是安全的。Reflect.enumerate(..)
,Object.keys(..)
,for..in
和JSON.stringify(..)
继续共享一个可观察的排序,但是排序不一定和Reflect.ownKeys(..)
,所以我们仍然需要小心。END