Skip to content

Latest commit

 

History

History
281 lines (221 loc) · 11.8 KB

元编程是什么.md

File metadata and controls

281 lines (221 loc) · 11.8 KB

2020.4.27

元编程的目标是利用语言自身的内省能力使代码的其余部分更具描述性、表达性和灵活性。

ES6在JavaScript现有的基础上为元编程新增了一些形式/特性。如下:

函数名称 程序中有多种方式可以表达一个函数,函数的”名称”应该是说什么并非总是清晰无疑的。

有词法名称的函数 在ES6中, 默认情况下函数的词法名称(如果有的话)也会被设为它的name属性。如果函数设定了name值,那么这个值通常也就是开发者中栈踪迹使用的名称。 没有词法名称的函数 推导。

(function(){ .. }); // name: 
(function*(){ .. }); // name: 
window.foo = function(){ .. }; // name: 
class Awesome { 
 constructor() { .. } // name: Awesome 
 funny() { .. } // name: funny 
} 
var c = class Awesome { .. }; // name: Awesome 
var o = { 
 foo() { .. }, // name: foo 
 *bar() { .. }, // name: bar 
 baz: () => { .. }, // name: baz 
 bam: function(){ .. }, // name: bam 
get qux() { .. }, // name: get qux 
set fuz() { .. }, // name: set fuz 
 ["b" + "iz"]: 
function(){ .. }, // name: biz 
 [Symbol( "buz" )]: 
function(){ .. } // name: [buz] 
}; 
var x = o.foo.bind( o ); // name: bound foo 
(function(){ .. }).bind( o ); // name: bound

export default function() { .. } // name: default 
var y = new Function(); // name: anonymous 
var GeneratorFunction = 
function*(){}.__proto__.constructor; 
var z = new GeneratorFunction(); // name: anonymous

默认情况下,name 属性不可写,但可配置,也就是说如果需要的话,可使用 Object.defineProperty(..) 来手动修改。

元属性

new.target 元属性,关键字new用于属性访问的上下文。

公开符号

Symbol.iterator

调用展开和for…of循环时,会自动使用Symbol.iterator。

Symbol.iterator表示任意对象身上的一个专门位置(属性),语言机制自动在这个位置上寻找一个方法,这个方法 构造一个迭代器来消耗这个对象的值。很多对象定义有这个符号的默认值。

然而,也可以通过定义 Symbol.iterator 属性为任意对象值定义自己的迭代器逻辑,即使 这会覆盖默认的迭代器。

Symbol.toStringTag与Symbol.hashInstance

最常见的一个元编程任务,就是在一个值上进行内省来找出它是什么种类,这通常是为了确定其上适合执行何种运算。对于对象来说,最常用的内省技术是 toString() 和instanceof。

function Foo() {} 
var a = new Foo(); 
a.toString(); // [object Object] 
a instanceof Foo; // true
 ES6 中,可以控制这些操作的行为特性:
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

原型(或实例本身)的Symbol.toStringTag符号指定了再[object ___]字符串化使用的字符串值。

原型(或实例本身)的Symbol.toStringTag符号指定了再[object ___]字符串化使用的字符串值。

Symbol.species

通过Symbol.species改写类的默认的构造器。

class Cool { 
 // 把@@species推迟到子类
static get [Symbol.species]() { return this; } 
 again() { 
return new this.constructor[Symbol.species](); 
 } 
} 
class Fun extends Cool {} 
class Awesome extends Cool { 
 // 强制指定@@species为父构造器
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

就像前面代码中 Cool 的定义那样,内置原生构造器上 Symbol.species 的默认行为是return this。在用户类上没有默认值,但是就像展示的那样,这个行为特性很容易模拟。

如果需要定义生成新实例的方法,使用 new this.constructorSymbol.species 模式元编程,而不要硬编码 new this.constructor(..) 或 new XYZ(..)。然后继承类就能够自定义 Symbol.species 来控制由哪个构造器产生这些实例。

Symbol.toPrimitive

抽象类型转换运算tpPrimitive,它用在对象为了某个操作(比如比较==或者相加+)必须被强制转换为一个原生类型值的时候。在ES6之前,没有办法控制这以行为。 而在ES6中,在任意对象值上作为属性的符号@@toPrimitivessysmbol都可以通过指定一个方法来定制这个ToPrimitive强制换转。

var arr = [1,2,3,4,5]; 
arr + 10; // 1,2,3,4,510 
arr[Symbol.toPrimitive] = function(hint) { 
 if (hint == "default" || hint == "number") { 
 // 求所有数字之和
return this.reduce( function(acc,curr){ 
return acc + curr; 
 }, 0 ); 
 } 
}; 
arr + 10; // 25

Symbol.toPrimitive方法根据调用ToPrimitive的运算期望的类型,会提供一个提示(hint)指定“string”、“number”或“default”(这应该被解释为”number”)。在前面的代码中,加法 + 运算没有提示(传入 “default”)。而乘法 * 运算提示为 “number”,String(arr)提示为 “string”。

如果一个对象与另一个非对象值比较,== 运算符调用这个对象上的ToPrimitive 方法时不指定提示——如果有 @@toPrimitive 方法的话,调用时提示为 “default”。但是,如果比较的两个值都是对象,== 的行为和 === 一样,也就是直接比较其引用。这种情况下完全不会调用 @@toPrimitive。

正则表达式符号

对于正则表达式对象,有4个公开符号可以被覆盖,他们控制着这些正则表达式在4个对应的同名String.prototypt函数中如何被使用。

  • @@match : 正则表达式的Symbol.match值是一个用于利用给定的正则表达式匹配一个字符串值的部分或全部内容的方法。
  • @@replace:正则表达式的Symbol.replace值是一个方法,String.prototypt.replace(..)用它来替换一个字符串内匹配模式的一个或多个字符串序列。
  • @@search:正则表达式的Symbol.search值试试一个方法,String.prototype.search(..)用它来在另一个字符串中搜索一个匹配给定正则表达式的子串。
  • @@split:正则表达式的Symbol.split值时一个方法,String.prototype.split(..)用它把字符串在匹配给定正则表达式的分隔符处分隔为子串。

如果你不够艺高人胆大的话,就不要覆盖内置正则表达式算法了! JavaScript 的正则表达式引擎经过高度优化,所以你自己的用户代码很可能会慢上许多。这类元编程简洁强大,但是只应该在确实需要或能带来收益的时候才使用。

Symbol.isConcatSpreadable

符号 @@isConcatSpreadable 可以被定义为任意对象(比如数组或其他可迭代对象)的布尔型属性(SymbolisConcatSpreadable),用来指示如果把它传给一个数组的 concat(..) 是否应该将其展开。 考虑:

var a = [1,2,3], 
 b = [4,5,6]; 
b[Symbol.isConcatSpreadable] = false; 
[].concat( a, b ); // [1,2,3,[4,5,6]]

Symbol.unscopables

符号 @@unscopables 可以被定义为任意对象的对象属性(Symbol.unscopables),用来指示使用 with 语句时哪些属性可以或不可以暴露为词法变量。 考虑:

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 
}

@@unscopables 对象中的 true 表示这个属性应该是 unscopable 的,因此会从词法作用域变量中被过滤出去。false 表示可以将其包含到词法作用域变量中。strict 模式下不允许 with 语句,因此应当被认为是语言的过时特性。不要使用它。因为应该避免使用 with,所以符号 @@unscopables 也没有太大意义。

代理 Proxy

ES6中新增的最明显的元编程特性之一是Proxy。 代理是一种由你创建的特殊的对象,它“封装”另一个普通对象——或者说挡在这个普通对象的前面。你可以在代理对象上注册特殊的处理函数(也就是 trap),代理上执行各种操作的时候会调用这个程序。这些处理函数除了把操作转发给原始目标 / 被封装对象之外,还有机会执行额外的逻辑。

每个代理处理函数在对应的元编程任务执行的时候进行拦截,而每个 Reflect 工具在一个对象上执行相应的元编程任务。每个代理处理函数都有一个自动调用相应的 Reflect 工具的默认定义。几乎可以确定 Proxy 和 Reflect 总是这么协同工 作的。

get(..) 通过 [[Get]],在代理上访问一个属性(Reflect.get(..)、. 属性运算符或 [ .. ] 属性运算符)

set(..) 通过 [[Set]],在代理上设置一个属性值(Reflect.set(..)、赋值运算符 = 或目标为对象属性的解构赋值)

deleteProperty(..) 通 过 [[Delete]], 从 代 理 对 象 上 删 除 一 个 属 性(Reflect.deleteProperty(..) 或(delete)

apply(..)(如果目标为函数) 通 过 [[Call]],将代理作为普通函数 / 方 法 调 用(Reflect.apply(..)、call(..)、apply(..) 或 (..) 调用运算符)

construct(..)(如果目标为构造函数) 通过 [[Construct]],将代理作为构造函数调用(Reflect.construct(..) 或 new)

getOwnPropertyDescriptor(..) 通过 [[GetOwnProperty]],从代理中提取一个属性描述符(Object.getOwnPropertyDescriptor(..)或 Reflect.getOwnPropertyDescriptor(..))

defineProperty(..) 通过 [[DefineOwnProperty]],在代理上设置一个属性描述符(Object.defineProperty(..)或 Reflect.defineProperty(..))

getPrototypeOf(..) 通 过 [[GetPrototypeOf]],得到代理的 [[Prototype]](Object.getPrototypeOf(..)、Reflect.getPrototypeOf(..)、proto、Object#isPrototypeOf(..) 或 instanceof)

setPrototypeOf(..) 通 过 [[SetPrototypeOf]],设置代理的 [[Prototype]](Object.setPrototypeOf(..)、 Reflect.setPrototypeOf(..) 或 proto)

preventExtensions(..) 通过 [[PreventExtensions]],使得代理变成不可扩展的(Object.prevent Extensions(..)或 Reflect.preventExtensions(..))

isExtensible(..) 通过 [[IsExtensible]],检测代理是否可扩展(Object.isExtensible(..) 或 Reflect.isExtensible(..))

ownKeys(..) 通过 [[OwnPropertyKeys]],提取代理自己的属性和 / 或符号属性(Object.keys(..)、Object.getOwnPropertyNames(..)、Object.getOwnSymbolProperties(..)、Reflect.ownKeys(..) 或 JSON.stringify(..))enumerate(..)通过 [[Enumerate]],取得代理拥有的和“继承来的”可枚举属性的迭代器(Reflect.enumerate(..) 或 for..in)

has(..) 通过 [[HasProperty]],检查代理是否拥有或者“继承了”某个属性(Reflect.has(..)、Object#hasOwnProperty(..) 或 “prop” in obj)

但是还是有些操作是无法代理的,typeof String() +

var obj = { a:1, b:2 }, 
 handlers = { .. }, 
 pobj = new Proxy( obj, handlers ); 
typeof obj; 
String( obj ); 

obj + ""; 
obj == pobj; 
obj === pobj

可取消代理 可取消代理用 Proxy.revocable(..) 创建,这是一个普通函数,而不像 Proxy(..) 一样是构 造器。除此之外,它接收同样的两个参数:target 和 handlers。和 new Proxy(..) 不一样,Proxy.revocable(..) 的返回值不是代理本身。而是一个有两个 属性——proxy 和 revode 的对象

var obj = { a: 1 }, 
 handlers = { 
 get(target,key,context) { 
 // 注意:target === obj, 
 // context === pobj 
 console.log( "accessing: ", key ); 
return target[key]; 
 } 
 }, 
 { proxy: pobj, revoke: prevoke } = 
 Proxy.revocable( obj, handlers ); 
pobj.a; 
// accessing: a 
// 1 
// 然后:
prevoke(); 
pobj.a; 
// TypeError