JavaScript元编程

元编程是一种与程序的创建相关联的程序设计,这些程序的创建会由于其工作而生成其他程序,或者在执行过程中会自行更改程序。 (维基百科)

用一种简单的语言,可以将JavaScript内部的元编程视为一种机制,使您可以根据任何操作实时分析和更改程序。 而且,很可能您每天都在编写脚本时以某种方式使用它们。


从本质上讲,JavaScript是一种非常强大的动态语言,可让您很好地编写灵活的代码:


/** *   save-    */ const comment = { authorId: 1, comment: '' }; for (let name in comment) { const pascalCasedName = name.slice(0, 1).toUpperCase() + name.slice(1); comment[`save${pascalCasedName}`] = function() { //   } } comment.saveAuthorId(); //  authorId comment.saveComment(); //  comment 

用于动态创建其他语言方法的类似代码通常可能需要特殊的语法或API。 例如,PHP还是一种动态语言,但是在其中它将需要更多的努力:


 <?php class Comment { public $authorId; public $comment; public function __construct($authorId, $comment) { $this->authorId = $authorId; $this->comment = $comment; } //       public function __call($methodName, $arguments) { foreach (get_object_vars($this) as $fieldName => $fieldValue) { $saveMethodName = "save" . strtoupper($fieldName[0]) . substr($fieldName, 1); if ($methodName == $saveMethodName) { //   } } } } $comment = new Comment(1, ''); $comment->saveAuthorId(); //  authorId $comment->saveComment(); //  comment 

除了灵活的语法外,我们还有许多用于编写动态代码的有用函数:Object.create,Object.defineProperty,Function.apply等。


更详细地考虑它们。


  1. 代码生成
  2. 使用功能
  3. 处理对象
  4. 反映API
  5. 符号
  6. 代理人
  7. 结论

1.代码生成


动态执行代码的标准工具是eval函数,它允许您从传递的字符串中执行代码:


 eval('alert("Hello, world")'); 

不幸的是,eval有许多细微差别:


  • 如果我们的代码以严格模式(“ use strict”)编写,则在eval中声明的变量将在调用eval代码中不可见。 同时,eval中的代码本身可以随时更改外部变量。
  • eval中的代码既可以在全局上下文中(如果通过window.eval调用),也可以在其中进行调用的函数的上下文中执行(如果只是eval,没有窗口)。
  • 当用较小的变量名替换变量名以减小大小时,由于JS的缩小会出现问题。 作为字符串传递给eval的代码通常不会碰到minifier,因此,我们可以使用旧的未最小化的名称开始访问外部变量,这将导致细微的错误。

解决这些问题有一个很好的选择- 新功能


 const hello = new Function('name', 'alert("Hello, " + name)'); hello('') // alert("Hello, "); 

与eval不同,我们总是可以通过函数的参数显式传递参数,并动态地为其提供上下文(通过Function.applyFunction.call )。 另外,创建的函数总是在全局范围内调用。


在过去,eval通常用于动态更改代码,因为 JavaScript的反射机制很少,没有评估就不可能做到。 但是在现代语言标准中,出现了更多的高级功能,并且现在很少使用eval了。


2.使用功能


JavaScript为我们提供了许多出色的工具来动态处理函数,使我们能够在运行时获取有关函数的各种信息并进行更改:


  • Function.length-允许您从函数中找到参数数量:


     const func = function(name, surname) { console.log(`Hello, ${surname} ${name}`) }; console.log(func.length) // 2 

  • Function.applyFunction.call-允许您动态更改此函数的上下文:


     const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce(); //   person.introduce.call({ name: '' }); //   

    它们之间的区别仅在于,在Function.apply中,函数的参数用作数组,在Function.call中,以逗号分隔。 经常在将此参数列表作为数组传递给函数之前使用此功能。 一个常见的示例是Math.max函数(默认情况下,它无法与数组一起使用):


     Math.max.apply(null, [1, 2, 4, 3]); // 4 

    随着新的点差运算符的出现您可以简单地编写以下代码:


     Math.max(...[1, 2, 4, 3]); // 4 

  • Function.bind-允许您从现有函数创建一个副本,但具有不同的上下文:


     const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce(); //   const introduceEgor = person.introduce.bind({ name: '' }); introduceEgor(); //   

  • Function.caller-允许您获取调用函数。 不建议使用它 ,因为它在语言标准中是不存在的,并且在严格模式下无法使用。 这是由于以下事实:如果各种JavaScript引擎实现了语言规范中描述的尾部调用优化,则调用Function.caller可能会开始产生错误的结果。 用法示例:


     const a = function() { console.log(a.caller == b); } const b = function() { a(); } b(); // true 

  • Function.toString-返回函数的字符串表示形式。 这是一项非常强大的功能,可让您检查函数的内容及其参数:


     const getFullName = (name, surname, middlename) => { console.log(`${surname} ${name} ${middlename}`); } getFullName.toString() /* * "(name, surname, middlename) => { * console.log(`${surname} ${name} ${middlename}`); * }" */ 

    收到函数的字符串表示形式后,我们可以解析和解析它。 例如,这可以用于提取函数参数的名称,并根据名称自动替换所需的参数。 通常,有两种解析方法:


    • 解析一堆常规数据,我们将获得可接受的可靠性水平(如果我们不涵盖所有可能的函数条目类型,则可能无法正常工作)。
    • 我们获得该函数的字符串表示形式,并将其放入完成的JavaScript解析器中(例如esprimaacorn ),然后使用结构化AST。 通过esprima进行AST解析的示例。 我还可以为Alexei Okhrimenko的解析器提供一份不错的报告


具有常规函数解析的简单示例:


获取函数参数列表
 /** *    . * @param fn  */ const getFunctionParams = fn => { const COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm; const DEFAULT_PARAMS = /=[^,]+/gm; const FAT_ARROW = /=>.*$/gm; const ARGUMENT_NAMES = /([^\s,]+)/g; const formattedFn = fn .toString() .replace(COMMENTS, "") .replace(FAT_ARROW, "") .replace(DEFAULT_PARAMS, ""); const params = formattedFn .slice(formattedFn.indexOf("(") + 1, formattedFn.indexOf(")")) .match(ARGUMENT_NAMES); return params || []; }; const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionParams(getFullName)); // ["name", "surname", "middlename"] 


获得身体机能
 /** *     . * @param fn  */ const getFunctionBody = fn => { const restoreIndent = body => { const lines = body.split("\n"); const bodyLine = lines.find(line => line.trim() !== ""); let indent = typeof bodyLine !== "undefined" ? (/[ \t]*/.exec(bodyLine) || [])[0] : ""; indent = indent || ""; return lines.map(line => line.replace(indent, "")).join("\n"); }; const fnStr = fn.toString(); const rawBody = fnStr.substring( fnStr.indexOf("{") + 1, fnStr.lastIndexOf("}") ); const indentedBody = restoreIndent(rawBody); const trimmedBody = indentedBody.replace(/^\s+|\s+$/g, ""); return trimmedBody; }; //       getFullName const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionBody(getFullName)); 


重要的是要注意,使用缩小器时,解析函数内部的代码本身及其参数都可以被优化,因此可以更改。


3.处理对象


JavaScript有一个全局Object对象,其中包含许多用于动态处理对象的方法。


这些语言中的大多数方法早已在该语言中存在并被广泛使用。


对象属性


  • Object.assign-方便地将一个或多个对象的属性复制到第一个参数指定的对象:


     Object.assign({}, { a: 1 }, { b: 2 }, { c: 3 }) // {a: 1, b: 2, c: 3} 

  • Object.keysObject.values-返回键列表或对象值列表:


     const obj = { a: 1, b: 2, c: 3 }; console.log(Object.keys(obj)); // ["a", "b", "c"] console.log(Object.values(obj)); // [1, 2, 3] 

  • Object.entries-[[key1,value1],[key2,value2]]格式返回其属性的列表:


     const obj = { a: 1, b: 2, c: 3 }; console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]] 

  • Object.prototype.hasOwnProperty-检查属性是否包含在对象中(不在其原型链中):


     const obj = { a: 1 }; obj.__proto__ = { b: 2 }; console.log(obj.hasOwnProperty('a')); // true console.log(obj.hasOwnProperty('b')) // false 

  • Object.getOwnPropertyNames-返回其自身属性的列表,包括枚举和非枚举:


     const obj = { a: 1, b: 2 }; Object.defineProperty(obj, 'c', { value: 3, enumerable: false }); //    for (let key in obj) { console.log(key); } // "a", "b" console.log(Object.getOwnPropertyNames(obj)); // [ "a", "b", "c" ] 

  • Object.getOwnPropertySymbols-返回其自身的列表(包含在对象中,而不在其原型链中):


     const obj = {}; const a = Symbol('a'); obj[a] = 1; console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(a) ] 

  • Object.prototype.propertyIsEnumerable-检查属性是否可枚举(例如,在for-in,for-of循环中可用):


     const arr = [ ' ' ]; console.log(arr.propertyIsEnumerable(0)); // true —  ' '   console.log(arr.propertyIsEnumerable('length')); // false —  length    


对象属性描述符


描述符允许您微调属性参数。 使用它们,我们可以在读取/写入任何属性(getter和setters-get / set),使属性不可变或不可枚举以及许多其他事情时方便地制作自己的拦截器。


  • Object.definePropertyObject.defineProperties-创建一个或多个属性描述符。 使用getter和setter创建自己的描述符:


     const obj = { name: '', surname: '' }; Object.defineProperty(obj, 'fullname', { //     fullname get: function() { return `${this.name} ${this.surname}`; }, //     fullname (     delete obj.fullname) set: function(value) { const [name, surname] = value.split(' '); this.name = name; this.surname = surname; }, }); console.log(obj.fullname); //   obj.fullname = ' '; console.log(obj.name); //  console.log(obj.surname); //  

    在上面的示例中,fullname属性没有自己的值,而是动态使用name和surname属性。 不必同时定义getter和setter-我们可以只保留getter并获得只读属性。 或者,我们可以在设置器中添加其他操作以及设置值,例如,日志记录。
    除了get / set属性外,描述符还具有其他几个要配置的属性:


     const obj = {}; //      get/set,       value.    get/set  value.   — undefined. Object.defineProperty(obj, 'name', { value: '' }); // ,         (for-in, for-of, Object.keys).   — false. Object.defineProperty(obj, 'a', { enumerable: true }); //         defineProperty     delete.   — false. Object.defineProperty(obj, 'b', { configurable: false }); //      .   — false. Object.defineProperty(obj, 'c', { writable: true }); 

  • Object.getOwnPropertyDescriptorObject.getOwnPropertyDescriptors-允许您获取所需的对象描述符或其完整列表:


     const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyDescriptor(obj, "a")); // { configurable: true, enumerable: true, value: 1, writable: true } /** * { * a: { configurable: true, enumerable: true, value: 1, writable: true }, * b: { configurable: true, enumerable: true, value: 2, writable: true } * } */ console.log(Object.getOwnPropertyDescriptors(obj)); 


在处理对象时创建限制


  • Object.freeze- “冻结”对象的属性。 这种“冻结”的结果是对象属性的完全不变性-无法更改和删除它们,添加新属性,更改描述符:


     const obj = Object.freeze({ a: 1 }); //       ,       . obj.a = 2; obj.b = 3; console.log(obj); // { a: 1 } console.log(Object.isFrozen(obj)) // true 

  • Object.seal- “密封”对象的属性。 密封与Object.freeze相似,但是有很多区别。 与Object.freeze中一样,我们禁止添加新属性,删除现有属性,更改其描述符,但同时我们可以更改属性值:


     const obj = Object.seal({ a: 1 }); obj.a = 2; //  a   2 //     ,       . obj.b = 3; console.log(obj); // { a: 2 } console.log(Object.isSealed(obj)) // true 

  • Object.preventExtensions-禁止添加新的属性/描述符:


     const obj = Object.preventExtensions({ a: 1 }); obj.a = 2; //       ,       . obj.b = 3; console.log(obj); // { a: 2 } console.log(Object.isExtensible(obj)) // false 


对象原型


  • Object.create-使用参数中指定的原型创建对象。 此功能可用于原型继承和创建“干净”对象,而无需Object.prototype的属性:


     const pureObj = Object.create(null); 

  • Object.getPrototypeOfObject.setPrototypeOf-获取/更改对象的原型:


     const duck = {}; const bird = {}; Object.setPrototypeOf(duck, bird); console.log(Object.getPrototypeOf(duck) === bird); // true console.log(duck.__proto__ === bird); // true 

  • Object.prototype.isPrototypeOf-检查当前对象是否包含在另一个对象的原型链中:


     const duck = {}; const bird = {}; duck.__proto__ = bird; console.log(bird.isPrototypeOf(duck)); // true 


4.反映API


随着ES6的到来,全局Reflect对象已添加到JavaScript中,以存储与反射和自省有关的各种方法。


他的大多数方法是将现有方法从诸如ObjectFunction之类的全局对象转移到一个单独的名称空间的结果,并进行了一些重构以使使用更加舒适。


将函数转移到Reflect对象不仅可以方便地寻找必要的反射方法并提供更大的语义,而且还可以避免在我们的对象原型中不包含Object.prototype的情况出现,但我们想从那里使用这些方法:


 let obj = Object.create(null); obj.qwerty = 'qwerty'; console.log(obj.__proto__) // null console.log(obj.hasOwnProperty('qwerty')) // Uncaught TypeError: obj.hasOwnProperty is not a function console.log(obj.hasOwnProperty === undefined); // true console.log(Object.prototype.hasOwnProperty.call(obj, 'qwerty')); // true 

重构使方法的行为更加明确和单调。 例如,如果在更早的时候,当对不正确的值(例如数字或字符串)调用Object.defineProperty时,会引发异常,但同时,对不存在的对象描述符调用Object.getOwnPropertyDescriptor时会默默返回undefined,然后Reflect的类似方法始终会为不正确的数据引发异常。


还添加了几种新方法:


  • Reflect.constructObject.create的更方便的替代方法,它不仅允许使用指定的原型创建对象,还可以立即对其进行初始化:


     function Person(name, surname) { this.name = this.formatParam(name); this.surname = this.formatParam(surname); } Person.prototype.formatParam = function(param) { return param.slice(0, 1).toUpperCase() + param.slice(1).toLowerCase(); } const oldPerson = Object.create(Person.prototype); // {} Person.call(oldPerson, '', ''); // {name: "", surname: ""} const newPerson = Reflect.construct(Person, ['', '']); // {name: "", surname: ""} 

  • Reflect.ownKeys-返回属于指定对象(而不是原型链中的对象)的属性的数组:


     let person = { name: '', surname: '' }; person.__proto__ = { age: 30 }; console.log(Reflect.ownKeys(person)); // ["name", "surname"] 

  • Reflect.deleteProperty- 删除操作符的替代方法,以一种方法的形式进行:


     let person = { name: '', surname: '' }; delete person.name; // person = {surname: ""} Reflect.deleteProperty(person, 'surname'); // person = {} 

  • Reflect.has - in运算符的替代方法,以一种方法的形式实现:


     let person = { name: '', surname: '' }; console.log('name' in person); // true console.log(Reflect.has(person, 'name')); // true 

  • Reflect.getReflect.set-用于读取/更改对象属性:


     let person = { name: '', surname: '' }; console.log(Reflect.get(person, 'name')); //  Reflect.set(person, 'surname', '') // person = {name: "", surname: ""} 


有关更改的更多详细信息,请参见此处


反映元数据


除了上面列出的“反射对象”方法外,还有一个实验性建议,可以方便地将各种元数据绑定到对象。


元数据可以是与对象没有直接关系的任何有用信息,例如:


  • TypeScript在打开embedDecoratorMetadata标志时,会在元数据中写入有关类型的信息,从而允许您在运行时访问它们。 此外,可以通过以下关键设计获取此信息:类型:
     const typeData = Reflect.getMetadata("design:type", object, propertyName); 
  • 流行的用于反转控制的InversifyJS库将有关所描述关系的各种信息存储在元数据中。

目前,此polyfill可在浏览器中使用


5.符号


符号是一种新的不可变数据类型,主要用于为对象属性标识符创建唯一的名称。 我们可以通过两种方式创建角色:


  1. 本地符号-Symbol函数的参数中的文本不会影响唯一性,仅在调试时才需要:


     const sym1 = Symbol('name'); const sym2 = Symbol('name'); console.log(sym1 == sym2); // false 

  2. 全局字符-字符存储在全局注册表中,因此具有相同键的字符相等:


     const sym3 = Symbol.for('name'); const sym4 = Symbol.for('name'); const sym5 = Symbol.for('other name'); console.log(sym3 == sym4); // true,        'name' console.log(sym3 == sym5); // false,     


创建此类标识符的能力使我们不必担心会覆盖我们未知对象中的某些属性。 这种质量使标准的创建者可以轻松地向对象添加新的标准属性,而不会破坏与各种现有库(可能已经定义了相同属性)和用户代码的兼容性。 因此,存在许多标准符号,其中一些提供了新的反思机会:


  • Symbol.iterator-允许您使用for-of... spread运算符创建用于迭代对象的自己的规则:


     let arr = [1, 2, 3]; //       arr[Symbol.iterator] = function() { const self = this; let pos = this.length - 1; return { next() { if (pos >= 0) { return { done: false, value: self[pos--] }; } else { return { done: true }; } } } }; console.log([...arr]); // [3, 2, 1] 

  • Symbol.hasInstance是一种确定构造函数是否将对象识别为其实例的方法。 由instanceof运算符使用:


     class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } console.log([] instanceof MyArray); // true 

  • Symbol.isConcatSpread-指示在Array.concat中串联时是否应展平数组:


     let firstArr = [1, 2, 3]; let secondArr = [4, 5, 6]; firstArr.concat(secondArr); // [1, 2, 3, 4, 5, 6] secondArr[Symbol.isConcatSpreadable] = false; console.log(firstArr.concat(secondArr)); // [1, 2, 3, [4, 5, 6]] 

  • Symbol.species-允许您指定将使用哪个构造函数在类内部创建派生对象。
    例如,我们有一个用于处理数组的标准Array类,它具有一个.map方法,该方法根据当前数组创建一个新数组。 为了找出用于创建该新数组的类,Array如下调用this.constructor [Symbol.species]


     Array.prototype.map = function(cb) { const ArrayClass = this.constructor[Symbol.species]; const result = new ArrayClass(this.length); this.forEach((value, index, arr) => { result[index] = cb(value, index, arr); }); return result; } 

    因此,重写Symbol.species,我们可以创建自己的用于处理数组的类,并说所有标准方法(例如.map,.reduce等)都不返回Array类的实例,而是返回我们类的实例:


     class MyArray extends Array { static get [Symbol.species]() { return this; } } const arr = new MyArray(1, 2, 3); // [1, 2, 3] console.log(arr instanceof MyArray); // true console.log(arr instanceof Array); // true //   Array.map     Array,    Symbol.species  this      MyArray const doubledArr = arr.map(x => x * 2); console.log(doubledArr instanceof MyArray); // true console.log(doubledArr instanceof Array); // true 

    当然,这不仅适用于数组,而且适用于其他标准类。 此外,即使我们仅使用返回相同类的新实例的方法来创建自己的类,也应使用this.constructor [Symbol.species]来获取对构造函数的引用。


  • Symbol.toPrimitive-允许您指定如何将我们的对象转换为原始值。 如果是更早的版本,为了简化为原始类型,我们需要将toString和valueOf一起使用,现在一切都可以通过一种方便的方法完成:


     const figure = { id: 1, name: '', [Symbol.toPrimitive](hint) { if (hint === 'string') { return this.name; } else if (hint === 'number') { return this.id; } else { // default return this.name; } } } console.log(`${figure}`); // hint = string console.log(+figure); // hint = number console.log(figure + ''); // hint = default 

  • Symbol.match-允许您为String.prototype.match函数的方法创建自己的处理程序类:


     class StartAndEndsWithMatcher { constructor(value) { this.value = value; } [Symbol.match](str) { const startsWith = str.startsWith(this.value); const endsWith = str.endsWith(this.value); if (startsWith && endsWith) { return [this.value]; } return null; } } const testMatchResult = '||'.match(new StartAndEndsWithMatcher('|')); console.log(testMatchResult); // ["|"] const catMatchResult = '|'.match(new StartAndEndsWithMatcher('|')); console.log(catMatchResult) // null 

    Symbol.replace , Symbol.search Symbol.split String.prototype .



, ( reflect-metadata ) . - , , . :


 const validationRules = Symbol('validationRules'); const person = { name: '', surname: '' }; person[validationRules] = { name: ['max-length-256', 'required'], surname: ['max-length-256'] }; 

6. (Proxy)


Proxy , Reflect API Symbols ES6, // , , . , .


, data-binding MobX React, Vue . .


:


 const formData = { login: 'User', password: 'pass' }; const proxyFormData = new Proxy(formData, { set(target, name, value) { target[name] = value; this.forceUpdate(); //   React- } }); //       forceUpdate()    React proxyFormData.login = 'User2'; //     ,   -      proxyFormData.age = 20; 

, /:


 const formData = { login: 'User', password: 'pass' }; const proxyFormData = {}; for (let param in formData) { Reflect.defineProperty(proxyFormData, `__private__${param}`, { value: formData[param], enumerable: false, configurable: true }); Reflect.defineProperty(proxyFormData, param, { get: function() { return this[`__private__${param}`]; }, set: function(value) { this[`__private__${param}`] = value; this.forceUpdate(); //   React- }, enumerable: true, configurable: true }); } //       forceUpdate()    React proxyFormData.login = 'User2'; //                  -  Reflect.defineProperty proxyFormData.age = 20; 

-, — Proxy ( , ), / , delete obj[name] .


7.


JavaScript , ECMAScript 4, . , .


You Don't Know JS .

Source: https://habr.com/ru/post/zh-CN417097/


All Articles