javascript chapter14 元编程
14.1 property attribute
1.writable 属性,property 的值是否可写
2.enumberable 属性,property是否可枚举,包括for/in Object.key()
3.configurable属性,property是否可删除,可被改变。
accessor property的4个属性是 get,set,enumerable configurable
这4个属性的查询方式要通过property descriptor这个对象。它有value ,writable enumerable configurable.
使用Object.getOwnPropertyDescriptor();获得property descriptor
//return {value:1,writable:true,enumerable:true,configurable:true}
Object.getOwnPropertyDescriptor({x:1},"x");
//read-only accessorproperty
const random={
get octet(){return Math.floor(Math.random()*256);}
};
//return {get:/*func*/,set:undefined,enumberable:true,configurable:true}
Object.getOwnPropertyDescriptor(random,"octet");
//return undefined for inherited properties and properties that don't exist
Object.getOwnPropertyDescriptor({},"x");//=>undefined no such prop
Object.getOwnPropertyDescriptor({},"toString");//=>undefined inherited
Object.getOwnPropertyDescriptor()只在自有properties时有效。查询继承property需要明确遍历protoType链。
设置Property的属性,要使用Object.defineProperty(),传递要修改的对象,需要创建或更改的property名,和一个property descriptor 对象
let o={};
//Add a non-numerable data property x with value 1
Object.defineProperty(o,"x",{
value:1,
writable:true,
enumerable:false,
configurable:true
});
//Check that the property is there but is non-enumerable
o.x//=》1
Object.keys(o)//==>[]
//Now modify the property x so that it is read-only
Object.defineProperty(o,"x",{wriable:false});
//try to change the property x so that it is read-only
o.x=2;//fails silently or throws TypeError in strict mode
o.x// 1
//The property is still configurable, so we can change its value like this:
Object.defineProperty(o,"x",{value:2});
//Now change x from a data property to an accessor property
Object.defineProperty(o,"x",{get:function(){return 0;}});
o.x//=>0
传递给Object.defineProperty()的property descriptor 不需要包括所有的4个属性。创建属性时,没有传递的属性会默认为false 或undefined
修改属性时,你未填写的属性不会被修改。
Object.create()方法,第一个参数是你要创建的对象的原型。第二个参数也可传递property descriptor
Object.defineProperty()或Object.defineProperties()会抛出TypeError
1.对象不可扩展,你却要新增Property
2.Property是不可configurable,你试图改变configurable enumerable
3.accessor property是不可configurable 你试图改变getter setter方法,或是将它改为数据property
4.data property是不可configurable,你想将它改变为accessor property
5.data property不可configurable,你要将它的writable属性从false改为true。前者不被允许。但将它从true改为false是允许的
6.data property是不configurable且不writable,这种情况下,你不能改变它的值。如果是configurable但不writable你却可以改变它的值。
Object.assign()只能拷贝可枚举属性(enumerable)
14.2 对象 扩展性
使用Object.isExtensible()来判断对象是否可扩展。
使用Object.preventExtensions()来使一个对象不可扩展。
Object.seal() 会使对象不可扩展,同时不可配置。对象不能新增属性,同时不能删除配置现有属性。现存属性如果可写writable可以被设置。
可以用Object.isSealed()判断对象是否是sealed
Object.freeze()更严格的锁定对象。不可配置,不可扩展,所有数据属性全部只读。但accessor proerties拥有setter方法的不受影响。
使用Object.isFrozen()来判断一个对象是否是frozen的。
Object.seal() Object.freeze()只影响给它传递的对象,不影响原型。如果你想影响原型,那么你就要对他们的原型链进行操作。
14.3 prototype属性
你可以通过Object.getPrototypeOf()获得对象原型
Object.getPrototypeOf({});//Object.prototype
Object.getPrototypeOf([]);//Array.prototype
Object.getPrototypeOf(()=>{});//Function.prototype
判断一个对象的原型是否是另一个对象可以使用isPrototypeOf()
let p={x:1};
let o=Object.create(p);
p.isPrototype(o); //true o inherits from p
Object.protype.isPrototypeOf(p);//true p inherits from Object.prototype
Object.protype.isPrototypeOf(o);//o does too
可以使用Object.setProtoTypeOf()来更改prototype
let o={x:1};
let p={y:2};
Object.setPrototypeOf(o,p);
o.y//2 o inherits the property y
let a=[1,2,3];
Object.setPrototypeOf(a,p);
a.join;//undefined a no longer has a join() method
14.4 著名 Symbols
14.4.1 Symbol.iterator 和 Symbol.asynclterator
这两个Symbol可使对象或者类可迭代或可异步迭代。具体见12章
14.4.2 Symbol.hasInstance
instanceof 方法,会查询对象的原型链。
但ES6以后,对象如果有Symbol.hasInstance方法,使用instanceof时,它会调用这个方法。我们可以用它做一些其他的事,比如
let uint8={
[Symbol.hasInstance](x){
return number.isInteger(x) &&x>=0&&x<=255;
}
};
128 instanceof uint8;//true
256 instanceof uint8;//false
Math.PI instanceof uint8;//false not a integer
14.4.3 Symbol.toStringTag
如果你调用基本对象toString()方法,你会得到
{}.toString()//"[object Object]"
如果你对内置属性的实例调用Object.prototype.toString()你会得到:
Object.prototype.toString.call([])//"[object Array]"
Object.prototype.toString.call(/./)//"[object RegExp]"
Object.prototype.toString.call(()=>{})//"[object Function]"
Object.prototype.toString.call("")//"[object String]"
Object.prototype.toString.call(0)//"[object Number]"
Object.prototype.toString.call(false)//"[object Boolean]"
利用上面的方法,可以得到一个小技巧:
function classof(o){
return Object.prototype.toString.call(o).slice(8,-1);
}
//调用
classof(null)//"Null"
classof(undefined)//Undefined"
classof(1)//"Number"
classof(10n**100n)//"BigInt"
classof("")//"String"
classof(false)//"Boolean"
classof(Symbol())//"Symbol"
classof({})//"Object"
classof([])//"Array"
classof(/./)//"RegExp"
classof(()=>{})//"Function"
classof(new Map())//"Map"
classof(new Set())//"Set"
classof(new Date())//"Date"
ES6之前,你使用这样的方法只能对内置类型使用,对你自建的类只会得到Object。Es6,Object.prototype.toString()会找寻Symbol.toStringTag属性
你可以使用下面的方式:
class Range{
get [Symbol.toStringTag](){return "Range";}
}
let r=new Range(1,10);
Object.prototype.toString.call(r);//"[Object Range]"
classof(r);//"Range"
14.4.4 Symbol.species
ES6之前你无法创建健壮的内置类的子类,ES6有了extends关键字
class EZArray extends Array{
get first(){return this[0];}
get last(){return this[this.length-1];}
}
let e=newEZArray(1,2,3);
let f=e.map(x=>x*x);
e.last//3
f.last//9
Array定义了concat(),filter(),map(),slice(),splice()方法返回一个Array。但子类调用这些方法会返回什么类型呢?ES6标准说会返回子类的类型。
它是这样实现的:
Array()构造函数有一个属性Symbol.species
当我们创建一个子类时,子类的构造器会继承父类的构造器,这样子类会有Symbol.species
map(),slice()这样的方法会创建并返回一个新数组,他们不会创建常规Array而是会调用new this.constructorSymbol.species创建新数组。
Array[Symbol.species]是只读的可访问属性,这个getter 函数会返回this.Subclass的构造器。
如果这样的行为不符合你的要求,你想让子类返回Array而不是子类你可以设置EZArray[Symbol.species]=Array;但是继承属性是只可读的你需要defineProPerty()
EZArray[Symbol.species]=Array;//失败
Object.defineProperty(EZArray,Symbol.species,{value:Array});//success
使用
class EZArray extends Array{
static get [Symbol.species](){return Array;}
get first(){return this[0];}
get last(){return this[this.length-1];}
}
let e=newEZArray(1,2,3);
let f=e.map(x=>x*x);
e.last//3
f.last//undefined: f is a regualr array with no last getter
14.4.5 Symbol.isConcatSpreadable
Array的concat()方法使用Symbol.species属性决定使用哪个构造器返回数组。concat()也使用Symbol.isConcatSpreadable。
ES6之前,concat()使用Array.isArray()判断一个值是否是数组。ES6的算法有了细微的改变。如果用于链接的参数是一个有Symbol.isConcatSpreadable的对象,
这个属性的布尔值决定了参数是否是可Spread的。如果这个属性不存在,那么Array.isArray()被使用。
有两种情况你想要使用这个Symbol
1.创建一个类数组对象,使他调用concat()方法时的行为像一个真正的数组。你可以简单添加这个属性,如
let arraylike={
length:1,
0:1,
[Symbol.isConcatSpreadable]:true
};
[].concat(arraylike)//->[1]
数组的子类默认情况下可Spread,你不想让这个子类Spread,当你使用concat()时,你可以:
class NonSpreadableArray extends Array{
get [Symbol.isConcatSpreadable](){return false;}
}
let a=new NonSpreadableArray(1,2,3);
[].concat(a).length//=>1 would be 3 elements long if a was sperad
14.4.6 模式匹配 Symbol(Pattern-Matching Symbol)
字符串类可以使用正则表达式进行模式匹配操作。ES6后,这些方法可以应用在正则表达式类或者定义了模式匹配行为的类中。
对于match() matchAll() search() replace() split()都有对应的Symbol:Symbol.match,Symbol.search 等等。
正则表达式对于文本的匹配是非常有用的。但在模糊匹配中太过复杂和不太管用。你可以利用Symbol来自己定义自己的模式类。
比如,你可以使用Intl.Collator来实现字符串比较。使用它可以匹配时候无视重音。
一般来说,如果你调用了上述的5种字符串方法 在一个模式对象中,它就像下面这样
string.method(pattern,arg)
这个调用就会变为:
pattern[symbol])(string ,arg)
举例,
class Glob{
constructor(glob){
this.glob=glob;
let regexpText=glob.replace("?","([^/]").replace("*","([^/]*);
this.regexp=new RegExp(`^{regexpText}$',"u");
}
toString(){return this.glob;}
[Symbol.search](s){return s.search(this.regexp);}
[Symbol.match](s){return s.match(this.regexp);}
[Symbol.replace](s,replacement){return s.replace(this.regexp,replacement);
}
}
14.4.7 Symbol.toPrimitive
类型转换。当类型转换为字符串时,javascript会调用toString()方法,如果未定义或返回的不是基础属性,会调用valueOf()方法。
如果要转换为数字类型,会首先调用valueOf()方法,如果未定义或者返回的不是基础属性,则会调用toString()方法。
ES6中,著名Symbol.Symbol.toPrimitive允许你覆盖前面的转换行为。给予你完全控制对象转换为基础类型的权利。要实现这点,定义一个名为symbolic name的方法。这个方法必须返回一个基础类型的值。这个方法接收一个字符串参数。告诉你转换应该怎么进行
如果参数为"string" ,说明你应该转换为字符串
参数为"number",你应该转换为数字
"default" 都可以,当转换上下文为+ ,==,!=等操作时。
实现方法为定义[Symbol.toPrimitive]方法。
14.6反射API
Reflect.apply(f,o,args)
这个函数调用对象o的方法f,参数列表是args
Reflect.construct(c,args,newTarget)
这个函数调用构造器c,类似于new关键字被使用,传递参数为args,如果newTarget参数被指定,就用它的值new.target伴随构造器的调用。如果没有被指定,new.target的值为c
Reflect.defineProperty(o,name,descriptor)
定义o对象内的属性,使用name作为名称,Descriptor对象要定义它的值,或get set方法,还有值的属性。Reflect.defineProperty()有点像Object.defineProperty(),但是返回布尔值,成功为true,失败为false
Reflect.deleteProperty(o,name)
删除对象o中名为name的属性。成功返回true,失败返回false
Reflect.get(o,name,receiver)
返回对象o中名为name的属性。如果属性有getter,同时如果receiver参数被指定,get函数会被作为receiver的方法被调用,而不是o的方法被调用。这个函数类似于o[name]
Reflect.getOwnPropertyDescriptor(o,name)
这个函数返回一个descriptor 对象,描述了o对象中名为name的属性。如果没有该属性,返回undefined。这个函数相当于Object.getOwnPropertyDescriptor()
Reflect.getPrototypeOf(o)
这个函数返回o的prototype,如果没有返回null。如果o是基础类型不是对象,会抛出TypeError的异常。这个函数相当于Object.getPrototype
Reflect.has(o,name)
如果o中存在名为name的属性,返回true。
Reflect.isExtensible(o)
如果o是extensible的,返回true.否则返回false。如果o不是对象,抛出TypeError异常。
Reflect.ownKeys(o)
这个函数返回o的所有属性的键(名称),如果o不是对象,抛出TypeError。返回数组中的元素的名称是字符串或Symbol。这个函数类似于Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()得到的结果的并集。
Reflect.prventExtensions(o)
设置对象o的extensible属性为false。使其不可扩展。如果成功返回true,失败false。o不是对象抛出TypeError
Reflect.set(o,name,value,receiver)
设置对象o的属性name的值为value。如果成功返回true,失败false。o不是对象抛出TypeError。如果receiver参数有值,setter方法会作为receiver的方法被调用,而不是o的方法。
Reflect.setPrototypeOf(o,p)
把对象o的原型Prototype值设为p。如果成功返回true,失败false。o不是对象抛出TypeError
14.7代理对象
代理类允许我们改变javascript对象的基本行为。
当我们创建代理对象时,我们需要说明两个其他对象,目标对象target object和控制对象handlers object
let proxy=new Proxy(target,handlers);
被Proxy 对象支持的操作和反射API定义的操作相同。
假设p使一个代理对象,你要删除p.x。Reflect.deleteProperty()函数和delete操作符的行为一致。当你使用delete操作符删除代理对象属性时,它会在handlers对象中查找deleteProperty()方法。如果这一方法存在,就调用此方法。如果不存在,代理对象会执行target object删除属性。
代理在所有的基本操作上都使用上述的工作方式。如果handlers对象存在一个合适的方法,就会进行这项操作(方法名和调整和反射函数一样),如果handlers对象不存在这个方法,代理就会使用基本操作,作用在target对象上。如果handlers对象为空,代理就仅仅是target对象的透明包装。
let t={x:1,y:2};
let p=new Proxy(t,{});
p.x//1
delete p.y//true
t.y//undfined this delete it in the target too
p.z=3;//define a new property on the proxy
t.z//3 define the property on the target
这种透明包装代理本质上等同于潜在目标对象。透明包装非常有用,但当你要创建可撤回代理时,不要使用Proxy()的构造器创建Proxy,而要用Proxy.revocable()工厂函数。这个函数返回Proxy对象和revoke()函数。当你调用revoke()函数时,代理立即会停止工作。
function accessTheDatabase(){/*省略实现过程*/ return42;}
let{proxy,revoke}=Proxy.revocable(accessTheDatabase,{});
proxy();//42
revoke();//the access can be turned off whenever we want
proxy();//!TypeError:we can no longer call this function
revocable代理实现了代码隔离,当你使用不信任的第三方库时可以使用这种方式。比如你想传递一个函数给不受你控制的库时,你可传递一个revocable代理作为代替,当你操作完库时可以撤销代理,这样阻止了库获得你函数的引用并在未预料的时候调用你的函数。
如果我们传递了一个非空handlers对象给Proxy()构造器,我们就不再定义了一个透明包装对象,取而代之的是实现我们定义的行为的代理。伴随着正确的设置handlers,
target object就变得不再相关了。
//we use a proxy to create an object that appears to have every possible property with the value of each property equal to its name
let identity=new Proxy({},{
//every property has its own name as its value
get(o,name,target){return name;},
//every property name is defined
has(o,name){return true;},
//There are too many properties to enumerate,so we just throw
ownKeys(o){throw new RangeError("Infinite number of properties");},
//All properties exist and are not writable,configurable or enumerable.
getOwnPropertyDescriptor(o,name){
return {
value:name,
enumerable:false,
writable:false,
configurable:false
};
},
//All properties are read-only so they can't be set
set(o,name,value,target){return false;},
//All properties are non-configurable.so they can't be deleted
deleteProperty(o,name){return false;},
//All properties exist and are non-configurable so we can't define more
defineProperty(o,name,desc){return false;},
//In effect,tihs means that the object is not extensible
isExtensible(o){return false;},
//All properties are already defined on this object,so it couldn't inherit anything even if it did have a prototype object.
getPrototypeOf(o){return null;},
//The object is nont extensible,so we can't change the prototype
setPrototypeOf(o,proto){return false;}
});
identity.x//"x"
identity.toString//"toString"
identity[0]//"0"
identity.x=1;//setting properties has no effect
identity.x;//"x"
delete identity.x//false can't delete properties either
identity.x//false can't delete properties either
identity.x//"x"
Object.key(identity);//RangeError,can't list all the keys
for(let p of identity);//RangeError
代理对象可以从目标对象和handlers对象中获得其行为,使用两个对象是非常有用的。
下述代码使用代理创建了一个目标对象的只读包装。当代码试图从对象中读时,它会读取target对象中的值,但如果代码试图更改对象和它的属性时,handler对象会抛出一个TypeError的异常。这样的代理非常有利于书写测试,你想写一个函数,这个函数用一个对象作参数,你的函数想保证这个对象不被改写。你传递一个只读包装对象,所有的写操作都会抛出异常。
function readOnlyProxy(o){
function readonly(){throw new TypeError("Readonly");}
return new Proxy(o,{
set:readonly,
defineProperty:readonly,
deletProperty:readonly,
setPrototypeOf:readonly,
});
}
let o={x:1,y:2};
let p=readOnlyProxy(o);
p.x;//1
p.x=2;//TypeError
delete p.y;//TypeError
p.z=3;//TypeError
p._proto_={};//TypeError
另一个技巧是当你写代理取定义一个handler方法去拦截对对象的操作,但仍然委派这些操作给target对象时。使用反射API非常简便。
下例是用代理委派所有操作给target对象但是使用Handler方法去记录这些操作。
//Return a Proxy object that wraps o,delegating all operations to that object after logging each operation,objname is a string taht
//will appear in the log messages to identify the object.If a has own properties whose values are objects or functions,then if you query
//the value of those properties ,you'll get a loggingProxy back,so that logging behavior of this proxy is contagious
function loggingProxy(o,objname){
//Define handlers for our logging Proxy object.
//Each handler logs a message and then delegates to the target object.
const handlers={
//This handler is a special case because for own properties whose value is an object or function,it returns a proxy
//rather than returning the value itself
get(target,property,receiver){
Console.log(`Handler get(${objname},${property.toString()})`);
let value=Reflect.get(target,property,receiver);
//If the property is an own property of the target and the value is an object or function,then return a proxy for it
if(Reflect.ownKeys(target).includes(property)&&
(typeof value==="object" || typeof value ==="function")){
return loggingProxy(value,`${objname}.${property.toString()}`);
}
//Otherwise return the value unmodified.
return value;
},
//There is nothing special about following three methods:they log the operation and delegate to the target objecct.
//They are a special csae simply so we can avoid logging the receiver object which can cause infinite recursion
set(target , prop, value,receiver){
console.log(`Handler set(${objname},${prop.toString()},${value})`);
return Reflect.set(target,prop,value,receiver);
},
apply(target,receiver,args){
console.log(`Handler ${objname}(${args})`);
},
construct(target ,args,receiver){
console.log(`Handler ${objname}(${}args})`);
return Reflect.construct(target,args,receiver);
}
};
//We can automatically generate the rest of the handlers
Reflect.ownKeys(Reflect).forEach(handlerName=>{
if(!handlerName in handlers){
handlers[handlerName]=function(target,...args){
console.log(`Handler ${handlerName}(${objname},${args})`);
};
}
});
return new Proxy(o,handler);
}
上述的loggingProxy()函数定义了代理,代理使用log记录了所有操作。如果你试图理解无文档的函数怎么调用你传递的独享,使用logging proxy吧!
下例,会使你对数组迭代有更深的理解。
//Define an array of data and an object with a function property
let data=[10,20];
let methods={square:x=>x*x};
//Create logging proxies for the array and the object
let proxyData=loggingProxy(data,"data");
let proxyMethods=loggingProxy(methods,"methods");
//suppose we want to understand how the Array.map() method works
data.map(methods.square);//[100,400]
//First,let's try it with a logging proxy
proxyData.map(methods,square)
//It produces this output:
//Handler get(data,map)
//Handler get(data,length)
//Handler get(data,constructor)
//Handler has(data,0)
//Handler get(data,0)
//Handler has(data,1)
//Handler get(data,1)
//Now lets try with a proxy methods object
data.map(proxyMethods.square)
//Log output:
//Handler get(method,square)
//Handler methods.square(10,0,10,20)
//Handler methods.square(20,1,10,20)
//Finally let's use a logging proxy to learn about the iteration protocol
for(let x of proxyData) console.log("Datum",x);
//Log output:
//Handler get(data,Symbol(Symbol.iterator))
//Handler get(data,length)
//Handler get(data,0)
//Datum 10
//Handler get(data,length)
//Handler get(data,1)
//Datum 20
//Handler get(data,length)
14.7代理不定量
前面定义的readOnlyProxy()函数创建了冻结的代理对象,任何试图改变属性的值,加或删属性都会抛出异常。但是只要target对象没有冻结时,我们使用
Reflect.Extensible()和Reflect.getOwnPropertyDescriptor()查询对象时,它会告诉我们可以set add delete属性。readOnlyProxy()就创建了
不一致状态的对象。我们可以修复这个问题,添加isExtensible() getOwnPropertyDescriptor()的handlers,或者我们放任不管这个问题。
Proxy handler API允许我们定义不一致的对象。但是代理类本身却阻止我们创建不一致代理对象。代理类在执行操作后会执行类型检查,比如:
let target=Object.preventExtensions({});
let proxy =new Proxy(target,{isExtensible(){return true;}});
Reflect.isExtensible(proxy);//TypeError