js 的发展是跟随着万维网一同发展起来的,占据浏览器市场,就占据桌面电脑的市场,所以 js 的意义流行倒今天,另外还得益于移动互联网的发展,js 天生就是为浏览器所使用的,所以事实证明语言设计的再烂,找到一个好的领域和好的宿主做寄生虫,也是不错选择。js 烂归烂但是对于想要刚刚学习入门编程语言的朋友来说确实是一个不错的选择,因为没有完整的类型系统和静态编程语言那么严格要求程序员编写代码的数据类型和规范,所以导致入门门槛也很低,学习成本也低,不过这样使得写出来的程序肯定是很烂。
我个人把 php 和 js 划分成为一类的,都是脚本和动态语言,不过 php 只专注于 web 领域,而 js 已经渗透到除了 web 之外的其他领域,嵌入式、桌面端 electron 技术、移动端的原生开的的 React Native 技术,最主要的还是的得益于 Node.js 运行时的出现,导致 js 可以脱离浏览器,在 Node.js 为宿主环境下运行,并且 Node.js 运行时采用的 V8 可以对写的再烂的代码进行运行时优化。
还有一些新运行时 wasm 运行时,这种目前还是一个设计阶段,草案阶段很多功能都是玩具,却被一些人吹上了天,怎么怎么牛逼的不得了。开发玩具和 Demo 容易,做一个生产级别还是有挑战,wasm 只是一大堆大厂玩家定制新的类似于二进制文件的编译标准格式,一些小公司却发现了其中红利,想赶上风口自己开发 wasm 运行时来抢占市场,目前市面上做的比较好 wasmer 也不错采用 Rust 编写,一部分采用 C++ 这种估计未来 wasm 生态不断发展,各种新特性往里面估计 C++ 维护成本也在哪里,还有一个 Go 语言实现运行时 wazero ,没有使用 cgo 而是使用纯 Go 语言实现的。
相比这些 GraalVM 则采用的是 Java 实现 hotsopt 的 jvmci 接口的 GraalWasm 是我目前最为看好的 wasm 运行时项目,目前 hotsopt 这款虚拟机已经大规模在生产上经过锻炼,而且已经很成熟,并且着很多这方面的专家,做多语言运行时和编译器,还有编程语言设计都需要是经验,而不是某些拿来个 Demo 玩具就跑出来骗投资人的钱的产品。但是目前 wasm 设计内存布局和 Java 编译目标设计内存布局还是有一点差异,例如 wasm gc 还处于草案阶段,但是这不影响 Java 和 wasm 之间操作,毕竟 wasm 是依赖于运行时的,而不是 Java 依赖于 wasm ,wasm 是可以跑在 GraalVM 之上的。
// 在 js 里面使用 var 变量注意全局作用 // 堆代码 duidaima.com arr = [1,2,3,4,5]; function array() { arr = "覆盖原理的值"; }; array(); console.log(arr); var i = 100; function scope() { // 提前被访问到了内存 console.log(s); var s = 12.3; console.log(s); var i = 0; console.log(i); if (true) { // 覆盖掉外面掉 i var i = 200; console.log(i); } console.log(i); }; scope(); console.log(i);
var 定义变量可以被全局访问,默认还没有类型信息,乃至全局访问这个变量;默认使用 var 关键字定义的变量作用域为函数作用域,而不是块级作用域。这意味着在函数内部定义的变量可以在整个函数内部访问,而不仅仅是在定义的块中。这可能会导致变量被误用或意外地被覆盖,导致某些函数访问到了不该被访问到变量,本应该是块作用域的变量提升到全局;即变量可以在声明之前被访问到,但其值为 undefined,这可能会导致一些错误。
// 在 js 里面还有数组类型 let arr = [1,2,3,4,5]; console.log(arr); // 打印长度 console.log("arr length:",arr.length); // 修改下标为 0 的元素的值 arr[0] = 100; console.log(arr); // 默认每个对象都方法,包括数组也有方法,预先设计好的 let empty_arr = []; empty_arr.push(1,2,3,4,5); console.log("empty_arr:",empty_arr); empty_arr.reverse(); console.log(empty_arr); // 通过指定下标的方式创建数组,并且指定一个 lenght 属性 const a = {0: 1, 1: 2, 2: 3, length: 3}; console.log(a); // {0: 1, 1: 2, 2: 3, length: 3} Array.prototype.reverse.call(a); //same syntax for using apply() console.log(a); // {0: 3, 1: 2, 2: 1, length: 3} // 生产环境禁止写这样的代码,因为自定义的 length 会覆盖掉原先的数组长度 console.log("array.length:",a.length);JS 虽然支持面对对象编程设计,但是没有像 C# 和 Java 那样严格的对对象的方法和属性做访问控制,不过目前最好解决方案是使用 TypeScript 进行编程。JS 的类型系统设计比较粗糙,特别是数值类型,在其他编程语言数值类型被分为多个子类型,例如 Java 中的 Double 和 Float 类型,在 Go 中 float32 和 float64 类型;而在 js 中浮点数和整数都属于一类为数值类型也就是 Number 类型,由于浮点数使用二进制来表示,因此可能存在精度问题,目前所有语言都面临这个精度丢失的问题,Java 中想要高精度则使用 BigDecimal 类型。在 JS 也存在精度丢失问题 0.1 + 0.2 的结果在 JavaScript 中并不是 0.3,而是一个非常接近 0.3 的数,如下的代码:
// 在 js 中数值类型和浮点类型,设计问题最多 let x = .3 - .2; let y = .2 - .1; // 肉眼看似相等,但是在计算机底层浮点数存储会出现问题 // 两个的计算结果是近似,而不是相等 console.log(x,y); console.log("x == y :",x===y); console.log("x == .1 :",x===.1); console.log(y); console.log("y == .1 :",y===.1);既然是因为 js 底层存储浮点数带来的问题,因为这些数字在计算机内部以二进制浮点数表示,而二进制无法精确表示某些十进制小数,既然存储小数上的误差,那我们可以使用相同的办法来解决这个问题,让减数和被减数的结果误差在合理范围内,我们让在逻辑让它们相等,例如下面的:
function isEqual(a, b) { const epsilon = 0.000001; // 定义一个误差范围 return Math.abs(a - b) < epsilon; } console.log(isEqual(0.3 - 0.2, 0.1)); // 输出 true除了整数和浮点数之外,JavaScript 还有一种特殊的数值类型:NaN(Not a Number),NaN 表示一个无效的数值,例如对一个非数字的值进行数学运算的结果就是 NaN,NaN 不等于任何值包括自身。并且 JS 中整除不会出现异常错误,而是简单返回一个正无穷或者负无穷,0 除以 0 是没有意义的,结果位 NaN 表示,NaN 既不是数值也不是空,如果要判断一个变量是不是为数值需要使用内置的 Number.isNaN(x) 来处理,判断一个变量是否为无穷也是使用内置的 Number.isFinite(x) ,例如下面代码:
// 无穷数值和非数值 let x = 0 / 0; // NaN console.log(x); // is NaN: true console.log("is NaN:",Number.isNaN(x)); // is Finite: false console.log("is Finite:",Number.isFinite(x)); // Infinity console.log(Number.MAX_VALUE * 2); // is Finite: false console.log("is Finite:",Number.isFinite(Number.MAX_VALUE * 2));为了解决大数值存储问题,在新的 ES 标准中添加了 BigInt 类型,通过字面量的的方式创建 1000n 或者使用 BigInt(10000) , 注意在 BigInt 中不能使用浮点数,如果使用了浮点数,将会导致 TypeError 错误,在 BigInt 类型和其他类型之间进行运算时需要进行类型转换,这可以通过调用 Number() 、String() 、Boolean() 等函数来实现;BigInt 与 Number 类型在内存使用和性能方面有所不同。BigInt 类型的内存使用量较大,而且执行速度也比较慢,因此当需要处理非常大的整数时,可以考虑使用 BigInt 类型,但对于一般的整数操作,建议使用 Number 类型。
// js 中 bigint 大数值类型 // 可以存储整数,不允许存储浮点数 var n = 10000n; var x = BigInt('293239129'); // n = 10000n console.log("n = ",n); // x = 293239129n console.log("x = ",x); // x typeof = bigint console.log("x typeof = ",typeof x);这都是在后面新的 js 标准中添加的类型,事实证明语言设计的再烂,只要后面官方愿意花时间和精力弥补之前设计缺陷,及时修正,还是有很多人使用的,如果是高精度浮点数可以使用第三方库来实现,其中最常用的库是 BigNumber.js 和 decimal.js 第三方库实现。
// 普通比较 true console.log(undefined == null); // 严格比较 false console.log(undefined === null); // node.js 环境下 两个原始类型都是不一样的 > typeof undefined 'undefined' > typeof null 'object'undefined 是一个名为 undefined 的类型,而 null 是一个 object 对象类型的关键字,这也很好解释两个在严格比较情况下为什么不相等,null 在运行过程中可以赋值给任何的定义变量语句使用,暂时改变变量状态所使用的;undefined 更多是指变量没有定义或者内存也没有分配情况下使用,下面代码可以证明:
let o = null; console.log(o); console.log(obj);互联网上经常使用这幅图作为 js 不同零值类型的比较,如下图:
// 对象类型和 null 值的关系 var people = null; console.log("people type:",typeof people); // people type: object function get() { console.log("people bool type:",typeof !people); // people bool type: boolean if (!people) { people = {name:"Leon",age:24}; } return people; } // { name: 'Leon', age: 24 } console.log(get());比较和相等逻辑运算时,js 作为一门动态语言采用的是主动类型转换,例如上面的 if (!people) 语句中 people 会被转换成 Boolean 类型做运算,此时带来一个问题程序中很多时候都需要做这种运算,js 默认的类型转换规则是什么?js 在常规运算的时候都是使用的 松散类型转换 和 隐式类型转换 进行的,当两个不同类型的值进行操作时,JavaScript 会自动进行松散类型转换,例如将字符串与数字相加,JavaScript 会将数字转换为字符串并将其与字符串拼接;当使用运算符或函数时,JavaScript 会自动执行隐式类型转换。例如在执行算术运算时,JavaScript 会将字符串转换为数字;显示类型转换也是支持的,如果在编程的时候明确知道类型和要做的运算预期,可以通过显示类型转换进行,例如下面的显示类型转换操作:
if (x === 10) { console.log("x is 10"); } else { console.log("x is not 10"); }没有完全可读性,并且依赖于 ; 符号来帮助 js 解释器来区分表达式之间的关系,如果分开来编写源代码,可以省略去表达式或者语句结尾的 ; ,如果没有正确得使用 ; 也会带来各种 bug ,例如:
// js 解释器会为 return 添加 ; return true; // 最后解释器得到语句是,错误 return; true;这也是我认为 js 设计没有严格要求是写 ; 还是不写 ; 带来的问题,像 Java 那样必须严格要求必须写上 ; ,统一一个标准,而不是两套标准都可以混着用。另外在新 es 版本中添加不少新的特性,例如先定义 ?? 符号,因为 js 早期的类型系统导致 0 、空字符串 、 false 都是为假值,在下面这种场景下就不试用了:
let max = maxWidth || perferences.maxWidth || 500;这个表达式的意思从左往右,如果 maxWidth 值没有意义则,则继续往找有意义的值,以此类推, 如果 perferences.maxWidth 也没有意义那么最好有意的是 500 ,但是我们预期的是 maxWidth 是有意义的,因为它的值是 0 则在默认运算时 js 解释器会认为它是没有意义的,因为如果是 0 则为 false !要改变结果则得使用先定义 ?? 符号,例如有一个 options 变量配置参数,为了保证参数合法性可以使用下面的方式:
// 先定义表达式,在某些时候 js 中的 0 、false 、undefined 是会有副作用 let options = { dir: null, level: 5, verbose: false, }; // 如果 dir 是没有值,则会使用先定义表达式设置的默认值 options.dir = options.dir ?? "/home/dings/test.txt"; console.log(options);通过这个例子可以看出来 js 设计并没有 Java 那么严谨但是在后面版本的 es 标准中添加了不少新特性语法糖,更容易解决问题,随心所欲编写代码。
let p = { name:"Tom", age:23, }; let arr = [1+2,0===0,p]; console.log(arr); console.log(arr[2].name); // 这样如果 arr 中没有第 4 个元素或者第 4 个元素没有 name 属性,程序也不会抛出异常 console.log(arr?.[3]?.name); // 语法糖,可链接访问 console.log(p?.nil); function bubble(arr) { for (let i = 0;i < arr.length - 1;i++) { for (let j = 0; j < arr.length - 1 - i;j++) { if (arr[j] > arr[j+1]) { [arr[j+1],arr[j]] = [arr[j],arr[j+1]]; } } } return arr; } // fn 参数有值则调用 function sort(arr,fn) { return fn?.(arr); } let array = [12,345,556,757,132,45,65]; // 正常访问 console.log(sort(array,bubble));有了条件式属性访问表达式,可以帮助程序员更容易编写程序和减少错误,在对某个对象属性或者方法或者数组访问时,不太确定是否正常访问到可以使用此条件式访问表达式进行操作。
// js 中创建对象方式,和对象的原型链 // 空对象 let empty = {}; // 有属性的对象 let point = {x:0,y:0}; // 复杂的对象 let pointer = { x: point.x, y: point.y, name: "pointer", }; console.log(pointer); // 通过内置的 Object 类型的方法创建 // 此种方式创建的有原型属性: Object.prototype let o1 = Object.create({x:1,y:2}); console.log(o1.x + o1.y); // 创建 null 的对象,不会继承任何东西,连 toString() 也没有用 let o2 = Object.create(null); console.log(o2); // 具有 Object.prototype 这样的属性 let o3 = Object.create(Object.prototype);我们并没有为我们的对象实现 toString() 方法,但是默认创建的对象,通过原型链我们可以访问到原型对象的属性和方法,甚至可以访问到 Object 对象的原型对象,也就是 Object.prototype 。这使得我们可以在任何对象上使用 Object 原型对象中定义的方法,例如 toString() 和 valueOf() 方法。JavaScript 中的继承是基于原型链的。每个对象都有一个原型对象,并且可以通过 prototype 属性访问它。原型对象也是一个对象,它也有自己的原型对象,这样就形成了一个原型链。当你尝试访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 引擎会在原型链上继续查找,直到找到该属性或者到达原型链的末 null 为止。这样,对象可以通过继承来访问其原型对象上的属性。
// 在 js 中没有类型系统,函数参数没有类型限制 function sum_v2(arr) { let total = 0; for (let ele of arr) { if (typeof ele === "number") { total += ele; } else { throw new TypeError("sum(): elements must be number."); } } return total; } // 因为元素不是数组类型,那么就会出现异常 // console.log(sum_v2("11",2,3,4,5,6,7)) console.log(sum_v2([1,2,3,4,5,6,7]))解决这样情况使用 typeof 或者使用 instanceof 关键字对实参进行操作,来处理符合预期的逻辑。绑定到对象身上的函数叫方法,默认对象方法在上面的文章中已经提到过,对象和字符串做 + 运算的时候会出现方法被隐式调用 toString() 方法。另外一个特殊例子是默认的数组排序算法是安装字符串的 Unicode 进行编码的,在 JavaScript 中,[1, 2, 10].sort() 这个排序问题的解决方案是使用一个自定义的比较函数,默认情况下 sort() 方法将数组元素视为字符串并进行排序,因此它会将数字转换为字符串并按照字典顺序进行排序。
// js 自有属性和原型链 let o = {}; o.x = 20; let d = Object.create(o); d.y = 23; let z = Object.create(d); console.log(z.toString()); let n = z.x + z.y; console.log(n);如上面代码所示原型链为 JavaScript 中对象之间的连接机制,每个对象都有一个内部链接到另一个对象的引用,这个对象被称为原型 Prototype ,当访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 就会沿着原型链向上查找,直到找到匹配的属性或方法或到达链的末尾(即原型链的顶部)。虽然此种方式可以解决对象之间关系,但是不能很好解决对象属性和方法访问限制,在 es5 版本中添加了,构造函数的支持和属性访问限制,如下面的代码:
// js 中如果对象的的 prototype 属性重新设置一个新对象, // 那么如果没有显示重新指定 constructor 属性, function Range(from, to) { this.from = from; this.to = to; } // 所有 Rnage 对象都继承这个对象 Range.prototype = { // 检查是否包含某个元素 includes: function (x) { return this.from <= x && x <= this.to; }, // 只适用数值范围,类似于 Go 语言中的通道传输消息 [Symbol.iterator]: function* () { for (let x = Math.ceil(this.from); x <= this.to; x++) yield x; }, // 返回范围的字符串表示,箭头函数中的 this 是在定义函数时确定的, // 它捕获了所在上下文的 this 值,并将其绑定到函数内部,无法通过 call()、apply() 或 bind() 来改变。 // toString: () => "(" + this.from + "..." + this.to + ")", toString: function () { return "(" + this.from + "..." + this.to + ")"; }, // 自引用属性构造函数 // constructor: Range 是在 Range.prototype 对象中定义的属性。 // 它指定了 Range 构造函数作为对象的构造函数。这意味着通过使用 new Range() 创建的对象, // 其 constructor 属性将指向 Range 构造函数本身。 // 这样做的目的是为了确保对象的正确类型信息。 // 当使用 instanceof 操作符检查对象类型时,它会通过检查对象的 constructor 属性来确定对象是否是由特定构造函数创建的。 constructor: Range, } let r = new Range(1, 10); console.log(r instanceof Range); let v = Object.create(r); v.x = 100, v.y = 200; v["sum"] = function () { console.log(this.x + this.y); } v.sum(); for (const v of r) { console.log(v); }在新的标准中如果一个函数默认为大写字母开头,则可以认为是一个对象构造函数,不需要使用 Object.create() 内置的函数进行创建对象。在最新的 es6 版本中可以设置对象可访问操作属性,并且有更新的方式定义类型的方式,内置了 class 关键字的支持,可以例如下面代码方式创建一个类型和它的属性,并且做访问控制:
// js 新的 es6 标准可以直接在 class 中编写属性, // 不需要编写 constructor 构造函数。 class Buffer { // 带有 # 号表示私有属性 #size = 0; #capacity = 4096; #buf = new Uint8Array(this.#capacity); // 只提供了 size 的方法进行访问 get size() { return this.#size; }; // 只能通过 add 方法添加元素 set add(v) { this.#buf[this.#size] = v; this.#size++; }; // 返回 buf 方法返回元素 get buf() { return this.#buf; }; toString() { return `Buffer { size = ${this.#size}; capacity = ${this.#capacity}; buf = ${this.#buf}; }`; }; } // 会有隐私的构造函数进行 let bf = new Buffer(); bf.add = 1; bf.add = 2; bf.add = 3; console.log(bf.size); // 打印初始化的 bf 对象 console.log(bf.toString()); // 为已经有的 class 扩张添加方法 // 这种方式添加新的属性时必须注意不要把以前已存在属性覆盖掉 // 或者新版本的原型对象新添加了同名的属性,就会覆盖掉这个属性 Buffer.prototype.contains = function(v) { for (let i = 0; i < this.size; i++) { if (this.buf[i] === v) { return true; } } return false; } console.log(bf.contains(20));可以忽略 constructor 关键字进行,使用 # 修饰的字段可以设置为私有属性,方法通过 set 和 get 关键字来控制方法的可操作性。面向对象最为基本的封装问题已经解决了,但是另外一个问题为如何实现对象之间相互依赖关系?如何定义抽象类?在 es6 中可以使用 extends 关键字来实现多个类型继承关系,已经存在了某个类型想对某个类型功能进行扩展,可以使用继承,例如下面代码:
// js 中类的继承关系,原型链 class EZArray extends Array { // 获取下标为第一个元素 get first() { return this[0]; } // 获取下标最后一个元素 get last() { return this[this.length - 1]; } } let a = new EZArray(); // 判断当前对象是否为某个类的实例 console.log(a instanceof EZArray); // 判断当前对象是否为 Array 实例,当前对象原型链上有 Array 所以 true console.log(a instanceof Array); a.push(1,2,3,4,5); console.log(a.pop()); // 我们自定义的方法 console.log(a.first); // 我们自定义的方法 console.log(a.last); // true console.log(Array.isArray(a)); // true console.log(EZArray.isArray(a));通过此种方式就可以复用父类的方法,也可以重写父类的方法,上面是为 Array 扩展了方法;另外方式为不实用 extends 关键字,而是之间使用组合的方式实现,弊病是必须要创建一个新的类型并且重写所以的方法:
// js 中的组合,而不是继承 // 有时候我们为了扩展某个类型的功能会为某个类型创建子类来重用父类的功能 // 也可以重写父类的方法,但是某种情况下需要添加更多功能,可以直接考虑创建 // 新的类型,将要重用的类型直接组合进去使用 class Histogram { // 初始化构造函数 constructor() { this.map = new Map(); } // 返回某个键出现的次数 count(key) { return this.map.get(key) || 0; } // 是否包含某个键 has(key) { return this.count(key) > 0; } // 返回某个键的大小 get size() { return this.map.size; } // 为某个键加一操作 add(key) { return this.map.set(key,this.count(key) + 1); } delete(key) { let count = this.count(key); if (count === 1) { // 如果只剩下一个了,则直接删除 this.map.delete(key); }else if (count > 1) { // 否则直接将其次数减一 this.map.set(key,count - 1); } } [Symbol.iterator]() { return this.map.keys(); } keys() { return this.map.keys(); } values() { return this.map.values(); } entries() { return this.map.entries(); } } let m = new Histogram(); m.add("Java"); m.add("Java"); m.add("Go"); m.add("😀"); m.add("😀"); m.add("😀"); m.add("😀"); m.add("JavaScript"); console.log(m.count("Java")); console.log(m.count("😀")); console.log(m.has("Java")); // 因为不是 Map 类型的实现,所以 false // Histogram 不是 Map 子类 console.log(m instanceof Map);最后只剩下了抽象类如何实现?很简单通过上面的方式可以看出来,直接通过 extends 关键字实现,先定义一个父类作为基类,子类必须继承此抽象类,重写抽象类中的方法,以此来实现 OOP 中多态表现,例如下面的代码:
// js 中的抽象类使用,js 没有原生提供 abstract 的支持 // 但是可以提供 extends 来实现某个预定义的类 // 抽象类可以作为一组子类的父类 // 抽象类型可以定义子类的部分功能实现让类型共享 class AbstractSet { // 不允许直接初始化创建 constructor() { if (new.target === AbstractSet) { throw new TypeError("AbstractSet not initialized, need to use extends subclasses to implement."); } } // 公共的抽象方法 has(x) {throw new Error("AbstractSet method.")} } // let aset = new AbstractSet(); class MySet extends AbstractSet { // 私有的 set 成员 #set = null; constructor() { super(); this.#set = new Set(); } has(x) { return this.#set.has(x); } add(v) { this.#set.add(v); } } let ms = new MySet(); ms.add(1); ms.add(2); ms.add(3); ms.add(4); console.log(ms.has(4)); console.log(ms.has(5));上面的代码中还是使用最新支持特性 new.target 来控制,抽象类不能直接使用 new 进行初始化创建抽象类单个实例,必须要使用子类继成实现抽象类才能通过 new 进行初始化。
// js 中对象数据属性和访问器属性元编程,用代码去操作代码 let obj = { x: 10, } // 获取对象的某一个属性的描述符 let ds = Object.getOwnPropertyDescriptor(obj, "x"); // { value: 10, writable: true, enumerable: true, configurable: true } console.log(ds); // 这个对象有一个只读的访问属性 const random = { get octet() { return Math.floor(Math.random() * 256); } } let ds2 = Object.getOwnPropertyDescriptor(random, "octet"); // { // get: [Function: get octet], // set: undefined, // enumerable: true, // configurable: true // } console.log(ds2); let ds3 = Object.getOwnPropertyDescriptor({}, "toString"); console.log(ds3 ?? "没有toString可访问属性"); // 为 obj 添加一个属性,并且设置属性参数 Object.defineProperty(obj, "y", { value: 100, writable: true, enumerable: false, configurable: true, }); // 100 console.log(obj.y); // [ 'x' ] 没有 y 因为 y 设置为了 不可枚举 console.log(Object.keys(obj)); // 修改 obj 的 y 属性可枚举 Object.defineProperty(obj, "y", { enumerable: true, }); // [ 'x', 'y' ] 可枚举的 console.log(Object.keys(obj)); // 添加一个设置一个访问属性为 octet Object.defineProperty(random, "octet", { set: function (v) { console.log(v) } }); // 再次获取到 random 属性信息 console.log(Object.getOwnPropertyDescriptor(random, "octet")); // 通过 random 的 octet 可访问属性设置值 random.octet = 10; // 批量设置某个对象的属性信息 let obj2 = Object.create(null); // 对一个空 obj2 对对象添加某个属性和可访问属性 Object.defineProperties(obj2, { x: { value: 1, writable: true, enumerable: true, configurable: true }, y: { value: 2, writable: false, enumerable: true, configurable: true }, t: { get() { return this.x * this.y; }, enumerable: true, configurable: true, } }); // 2 console.log(obj2.t); console.log(Object.getOwnPropertyDescriptors(obj2));所谓的元编程就是通过代码去操作代码逻辑,通过可编程的方式去操作代码,可能很绕口,常规编程方式是通过代码去操作数据,而元编程是通过写代码去操作其他代码。通过元编程并意味着可以不假思索的去使用,例如下面问题代码:
// js 中对象属性的可配置和可写冲突 let obj = Object.defineProperty({}, "x", { writable: false, configurable: true, value: 10, }); // 10 console.log(obj.x); // 不可写 obj.x = 100; // 10 console.log(obj.x); // 因为初次创建的 obj x 属性配置的是可配置 Object.defineProperty(obj, "x", { value: 200, }) // 此时已经被修改了 console.assert(obj.x === 200);上面这段代码的问题是一个对象的数据属性设置了不可写只读,但是是可以配置,如果编程高手就可以通过可配置的方式修改掉只读属性。
// js 中的反射特性使用,Reflect 对象提供一系列的关于反射的 API 函数。 let obj = { name: "Leon", } function f(params) { console.log(this); console.log(params); } // 将 f 函数绑定到 obj 对象上,并且传入一组参数数组 Reflect.apply(f, obj, ["Leon"]); class People { name = ""; age = 0; constructor(name, age) { this.name = name; this.age = age; // 默认 [class People] console.log(new.target); } } // 通过反射创建一个类型的对象 let p = Reflect.construct(People, ["Leon", 24]); // People { name: 'Leon', age: 24 } console.log(p); // 通过反射创建一个类型的对象,指定 new.target let p2 = Reflect.construct(People, ["Leon", 24], Object); // [Function: Object] // People { name: 'Leon', age: 24 } console.log(p2); var obj2 = { x: 1, y: 2 }; let x = Reflect.get(obj2, "x"); // 1 console.log(x); // Array let y = Reflect.get(["zero", "one"], 1); // "one" console.log(y);反射编程更关注的是在运行时通过检查和修改对象的结构和行为来动态操作代码的能力,而普通元编程针对程序代码本事,针对操作对象本身一些能力控制,常见形式是通过操作对象的属性、方法和原型来改变对象的行为。例如通过修改一个对象的原型,可以为其添加新的方法或覆盖现有的方法,从而改变对象的行为,下面代码:
// js 中的对象原型链 console.log(Object.getPrototypeOf({})); console.log(Object.getPrototypeOf([])); console.log(Object.getPrototypeOf(() => { })); // 判断一个对象是为某一个对象的原型 let p = { x: 1 }; let o = Object.create(p); // true p 对象是否为 o 的原型 console.log(p.isPrototypeOf(o)); let arr = [1, 2, 3, 4, 5]; // 1,2,3,4,5 console.log(arr.join()); // 把 arr 原型设置为 p ,此时就会失去默认的 join 方法 Object.setPrototypeOf(arr, p); // undefined 运行时异常 // console.log(arr.join()); let c = { z: 3 }; let d = { x: 1, y: 2, // 通过 __proto__ 字面量的方式设置 d 的原型为 c __proto__: c }; // { x: 1, y: 2, __proto__: { z: 3 } } console.log(d); // true console.log(c.isPrototypeOf(d));