JavaScript元编程学习笔记

元编程的概念

元编程是指写代码去操作其他代码,而常规编程是写代码操作数据。(但是其他的代码也可以看作数据,那也就没什么不同,也就不存在元编程了)
常见的元编程,有这么几种:

  • 动态生成代码。代码可以是相同或不同的编程语言,还可以在生成后执行它。
  • 改变语言的语法特性。并不是真的新增什么语法关键字,而是用编程语言的既有语法(如函数)来实现新增关键字的效果,在js中称为模板函数。
  • 改变程序的运行时特性。在js里,刨除原始类型外,一切皆是对象。即使是原始类型,也大多有对应的包装类。除了number类型,其他类型的字面量都可以当作对象直接使用,因为js引擎解析会隐式地创建一个对应的包装对象,并在使用完成后删除。

JS的元编程

JavaScript对象属性特性

对于数据属性,有value,writable,enumerableconfigure四个特性;
对于访问器属性,有get,set,enumerableconfigure四个特性。
描述这些特性的数据叫做属性描述符,是一个有着特定属性(以上特性的名字)的对象。
我们可以通过改变这些特性来改变对象的行为。有如下方法:

  • Object.getOwnPropertyDescriptor(Object, name): 获取指定对象指定属性的属性描述符对象。
  • Object.defineProperty(object, name, propertyDescriptor): 为对象新增具有指定特性的属性或改变已有属性的特性。propertyDescriptor缺省的特性值会默认取undefined或false。若创建或修改属性特性的行为是不被允许的(与特性冲突),将抛出TypeError。
  • Object.defineProperties(object, propertyDescriptors): 一次对对象的多个属性特性进行新增或修改。propertyDescriptors是一个将属性名映射到属性描述符的键值对。
  • Object.create(prorotype, propertyDescriptors): 创建对象并为其属性指定特性。第二个参数是可选的。

writable控制对value特性的修改;configurable控制对其他特性的修改,同时也控制着属性是否可以被删除。
如果对象不可扩展,可以修改其既有属性的特性,但是不能添加新属性。
若属性(数据属性与访问器属性)不可配置,不能修改其configurableenumerable特性。
若访问器属性不可配置,不能修改其get/set特性。
若数据属性不可配置,不能将其改为访问器属性。
若数据属性不可配置不可写,不能修改其值;而若数据属性可配置但不可写,可以通过先将其配置为可写后再修改值并配置为不可写的方式来变相的修改其值。

对象可扩展

控制是否可以给对象添加新属性。有两个方法:

  • Object.isExtensible(objet): 检查对象是否可扩展
  • Object.preventExtensions(object): 使对象不可扩展

创建的对象默认是可扩展的,而将对象修改为不可扩展的操作是不可逆的。
除了以上两个方法外,js还提供了两个便捷方法,用来在使对象不可扩展同时还改变其属性的特性:

  • Object.seal(object): 封存对象。使对象不可扩展并使其所有属性不可配置,但可写的的属性依然可写。
  • Object.freee(object): 冻结对象。使对象再不可修改,不可扩展。其属性不可配置,不能修改其属性的可枚举性。不能修改属性值,即使是访问器的getter/setter(但是因为访问器属性是函数调用,给人一种属性值还可以被修改的错觉)

prototype特性

指定对象从哪里继承属性

  • 使用new创建的对象使用构造函数的prototype属性作为其原型;使用字面量创建的对象使用Object.ptototype作为其原型;使用Object.create()创建的对象使用第一个参数作为其原型;
  • 使用Object.getPrototypeOf(object)来获取对象的原型;
  • 要确认一个对象是不是另一个对象的原型(原型链上的任何一个),使用Object.prototype.isPrototypeOf(),如:
    Object.prototype.isPrototypeOf(new Object)
  • 使用Object.setPrototype(object, prototype)来修改对象的原型;
  • Object.prototype 是不可变原型的特异对象,其原型始终为 null,不能修他的原型。
  • 虽然多数浏览器出于兼容性考虑还支持__proto__属性,但是它已经被废弃了。它是可读写的,它可以在使用对象字面量定义对象时作为属性来为对象指定原型。

公认符号Symbol

在ES6及以后标准里,新增了许多特性,比如异步编程、对象可迭代等。既要实现这些特性,还不能破坏既有代码。那么给对象新增任何字符串的属性,都有破坏既有代码的风险,所以新增了一个类型,即Symbol类型。
Symbol是一个函数对象(非构造函数),创建一个Symbol类型的值只需要调用函数即可。除此之外,Symbol还有一些成员,将它们定义为对象的成员,用于扩展对象来支持新的特性。这些行为可能会改变对象的运行时特性。

  • Symbol.iterator / Symbol.asyncIterator: 使对象可迭代/异步可迭代
  • Symbol.hasInstance: 用于判断某对象是否为某构造器的实例,它定义为类的静态方法。只能使用ES6语法定义,直接修改构造函数对象的属性是无效的。
    // 正确的定义方式
    class Fruit {
      static [Symbol.hasInstance] (instance) {
        return instance.type && instance.type === 'fruit';
      }
    }
    // 错误的定义方式
    function Fruit () {};
    Fruit[Symbol.hasInstance] = function (instance) {
      return instance.tyoe && instance.type === 'fruit;
    }
    
  • Symbol.toStringTag: 作为类的属性存在,用于创建对象的默认字符串描述。由 Object.prototype.toString()方法内部访问。
      class Foo {
        get [Symbol.toStringTag] () {return 'foo';}
        // 或者
        // [Symbol.toStringTag] = 'foo';
      }
      
      function Bar () {};
      Bar.prototype[Symbol.toStringTag] = 'bar';
    
      console.log(
        Object.prototype.toString.call(new Foo()), 
        Object.prototype.toString.call(new Bar())
      );
    
  • Symbol.species: 是构造函数对象的函数值属性,用以创建派生对象,它是一个只读的获取器属性。改变其值以覆盖对象的默认构造函数。
    构造函数默认的[Symbol.species]()属性值只是简单的返回this,子类构造函数继承这个属性,这意味着所有的子类构造函数都是它自己的“物种”。
    比如数组的concat()slice()map()filter()splice()方法都会返回新的数组对象,它们会调用new this.constructor[Symbol.species]()来创建这个新的数组实例。
      [].concat([]) instanceof Array; // true
      // 更改[Symbol.species]属性
      Object.defineProperty(Array, Symbol.species, {get: function() {return Object}});
       [].concat([]) instanceof Array; // false
      [].concat([]) instanceof Object; // true,实际上被转换为Number类型
    
  • Symbol.isConcatSpreadable: 布尔值属性。用于配置调用Array.prototype.concat()方法时,是否展开参数对象以及调用对象的数组元素。它一般有两个应用场景:
  1. 对数组及其子类对象不予展开
  let arr = [1,2,3];
  arr[Symbol.isConcatSpreadable] = false;
  [].concat(arr); // [[1,2,3]]
  class MyArray extends Array {
    [Symbol.isConcatSpreadable] = false;
  }
  new MyArray(1,2,3).concat([4,5,6]); // MyArray(4) [MyArray(3), 4, 5, 6, Symbol(Symbol.isConcatSpreadable): false]
  [4,5,6].concat(new MyArray(1,2,3)); // [4, 5, 6, MyArray(3)]
  1. 展开Array-Like对象(指有length属性以及可以将属性名转换为Number类型的属性)
  let arrayLike = {
    [Symbol.isConcatSpreadable] = true,
    length: 2,
    0: "hello",
    1: "world"
  };
  [].concat(arrayLike); // ['hello', 'world']

模式匹配符号

除了Strig的几个使用RegExp对象参数执行模式匹配的方法外,在ES6及以后的版本中,还可以使用以符号名作为属性名定义了模式匹配的对象。
String的match(),matchAll,search(),replace(),split()都有同名的符号。在调用字符串方法时,会自动调用模式对象上相应的符号名命名的方法。

  // str.method(pattern, arg) => pattern[Symbol.method](str, arg)
  class Replace1 {
    constructor(value) {
      this.value = value;
    }
    [Symbol.replace](string) {
      return `s/${string}/${this.value}/g`;
    }
  }
  console.log('foo'.replace(new Replace1('bar')));

Symbol.toPrimitive

函数属性,覆盖将对象转换为原始值时的默认算法。
有三个默认算法:

  1. 若要转换为字符串,先调用toString(),若未定义或返回值不是原始值,还会再调用valueOf();
  2. 若要转换为数字,先调用valueOf(),若未定义或返回值不是原始值,还会再调用toString();
  3. 若字符串或数字都可,JS会让类自己做决定。Date类型优先调用toString(),其他对象则优先调用valueOf();

该成员方法接收一个字符串类型的参数(string|number|default),该参数表示JS希望转换的原始类型。

  let obj = {
    [Symbol.toPrimitive](hint) {
      switch (hint) {
          case 'number': return 42;
          case 'string': return '四十二';
          default: return null;
      }
    }
  };
  Number(obj);obj++; //42
  String(obj);`${obj}`; // '四十二'
  obj+''; // 'null'
  obj+1; // 1。转换为null,在转换为0

Symbol.unscopable

它是针对废弃的with语句所导致的兼容问题而引入的变通方案。
with语句会取得一个对象,在其语句体中,就可以像使用变量一样使用该对象的属性。
在ES6及之后的版本中,with再取得对象时,会计算Object.keys(obj[Symbol.unscopable]),并在创建执行语句体的模拟作用域时忽略名字包含在计算结果的数组中的属性。
ES6使用该机制为构造函数的原型添加新的成员,这样就不会破坏既有代码。

let person = {
    name: 'Book',
    age: '18',
    gender: 'male'
}
person[Symbol.unscopables] = {
    gender: true
}
with (person) {
    console.log(name, age);
    console.log(gender)
}
// Book 18
// Uncaught ReferenceError: gender is not defined

这意味着可以使用Object.keys(Array.prototype[Symbol.unscopables])来取得所有ES6及以后为Array新增的方法。

  console.log(Object.keys(Array.prototype[Symbol.unscopables]));

模板标签

求值为函数的表达式右侧若是一个模板字面量,那就会转换为一个函数调用。字面量称为标签化模板字面量,函数称为标签函数
以“标签函数”形式被调用的函数会收到至少一个参数,分别是将模板字面量以插入值拆分后的字符串数组插入值。我们可以使用扩展操作符...将所有插入值参数收集到一个数组中。
String.raw函数就使用了模板标签特性。
当标签函被调用时,它收到的第一个参数有一个名为'raw'的属性,它也是一个数组。与参数本身不同的是,参数本身包含的字符串都已经解释了转义序列,如''转义为'\'。而该属性包含的字符串均未转义,如果希望使用反斜杠而又不想写两次反斜杠,那么这个属性会有用。

  let name = 'Tom', age = '2', gender = 'boy', hobby = 'paly with Jerry';
  
  function selfIntro(strs, n, g, a, h) {
    console.log('strs', strs);
    console.log('strs.raw', strs.raw);
    console.log(n, g, a, h);
  }
  
  function selfIntro(strs, ...vals) {
      console.log('strs', strs);
      console.log('strs.raw', strs.raw);
      console.log(vals.length, vals[0], vals[1], vals[2], vals[3]);
  }

  selfIntro`i am ${name},a ${gender},${age} years old,my hobby is ${hobby}.`

反射API: Reflect

与Math对象一样,Reflect不是类(函数对象),只是一个属性定义为一组静态函数的普通对象,不可构造。
反射对象的方法名与构造代理对象时指定的处理器对象一致。
Reflect并没有提供新的特性,它的方法复制了对象的各种既有的功能特性。

  • Reflect.apply(target, thisArgument, argumentsList)
    对一个函数进行调用,三个参数:函数对象、this值、由参数组成的数组。类似Function.prototype.apply()
  • Reflect.construct(target, argumentsList[, newTarget])
    对构造函数进行 new 操作,相当于执行 new target(...args)。可选的newTarget参数为构造函数调用中的new.target指定值,若未指定,则其值为构造函数本身。
  • Reflect.defineProperty(target, propertyKey, attributes)
    和 Object.defineProperty() 类似。如果设置成功就会返回 true
  • Reflect.deleteProperty(target, propertyKey)
    从对象删除属性,相当于执行 delete target[name]。
  • Reflect.get(target, propertyKey[, receiver])
    获取对象身上某个属性的值,类似于 target[name]。若属性有获取器,则可选的receiver属性作为获取器函数调用时的this值。
  let obj = {
    nickname: 'Foo',
    get name() {
      return this.nickname;
    },
  }
  let receiver = {
    nickname: "Bar"
  }
  obj.name; //'Foo'
  Reflect.get(obj, 'name'); //'Foo'
  Reflect.get(obj, 'name', receiver); //'Bar'
  • Reflect.getOwnPropertyDescriptor(target, propertyKey)
    类似于Object.getOwnPropertyDescriptor()。如果对象中有指定属性,则返回其属性描述符,没有则返回undefined。

  • Reflect.getPrototypeOf(target)
    获取对象的原型,类似于Object.getPrototypeOf()。若没有原型返回null。若参数非对象,抛出TypeError,而Object.getPrototypeOf()只在参数为null或undefined时奥驰TypeError,而其他类型的原始值会被转换为对应的包装对象。

  • Reflect.has(target, propertyKey)
    判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。

  • Reflect.isExtensible(target)
    检查对象是否可扩展,类似于 Object.isExtensible()。

  • Reflect.ownKeys(target)
    返回一个包含所有自身属性(不包含继承属性)的数组,结果可能包含字符串与符号。相当于调用Object.getOwnPropertyNames()与Object.getOwnPeopertySymbols(),并将返回的结果合并。
    与Object.keys()不同,Reflect.ownKeys()不收属性可枚举特性的影响。

  • Reflect.preventExtensions(target)
    使对象不可扩展,类似于 Object.preventExtensions()。返回一个Boolean。

  • Reflect.set(target, propertyKey, value[, receiver])
    给对象的属性分配值,可选的参数receiver指定属性的setter调用时的this值。返回一个Boolean值。

  • Reflect.setPrototypeOf(target, prototype)
    给对象设置原型。成功返回true,失败返回false。若对象不可扩展或操作本身会导致循环原型链(如将对象A的原型设置成对象B以后,再将对象B的原型设置为A就会导致循环)

代理对象 Proxy

介绍

Proxy是ES6新增的元编程特性。它用于创建一个对象的代理,从而实现对该对象的基本操作的拦截与自定义(如属性查找、赋值、枚举、函数调用等)。
创建一个代理器对象,要指定要代理的目标对象与处理器对象。处理器对象是一个由特定的名称命名的属性组成的键值对,这些属性称为代理的捕获器。
对代理对象执行某个操作时,若处理器存在对应的捕获器,则该捕获器执行这个操作;若不存在,就在目标对象执行该操作。
处理器对象的捕获器名称与Reflect对象的方法名一一对应,参数一样,所捕获的操作也与反射API对对象的操作一致。

用法

let target = {},
    handler = {
                has: function(target, propertyKey) {
                  console.log('target has: ' + Reflect.has(target, propertyKey));
                  return true;
                }
    },
    proxy = new Proxy(target, handler);
console.log('a' in target); // false
let has = 'a' in proxy; // target has: false
console.log(has); // true

透明包装代理与可撤销代理

传入一个空的处理器对象创建的代理就是透明包装代理,它本质上就是底层对象。看起来这样做似乎没什么意义,但是它在创建可撤销的代理对象时有用。
一个代理一旦被撤销,将不能进行任何代理操作。
创建一个可撤销的透明包装代理,既不会改变对象的行为,又可以在撤销代理后保护目标对象不再被修改。

使用Proxy.revocable(target, handler)来创建一个可撤销的代理对象。它返回一个对象,结构为:
{"proxy": proxy, "revoke": revoke}
proxy是代理对象,revoke是撤销代理的函数。

代理不变式

Proxy遵循JavaScript的不变式原则。
简单来说,对于默认状态(可扩展、属性可配置)的对象,JS允许代理的捕获器行为与目标对象的行为不一致。
而对于不可扩展的对象以及对象不可配置的属性,若代理行为与目标行为不一致,就会抛出TypeError。
比如目标对象不可扩展,而代理的处理器的isExtensible()操作却返回true,则会抛出异常
再比如目标对象的参数不可配置不可写,但是处理器的get()操作却返回了与目标对象属性值不一致的结果,也会报错。

posted @ 2023-05-14 01:15  钰琪  阅读(39)  评论(0编辑  收藏  举报