Class

Class 的基本语法 简介 类的由来 JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。 function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2); 上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。 基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } 上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。 Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。 ES6 的类,完全可以看作构造函数的另一种写法。 class Point { // ... } typeof Point // "function" Point === Point.prototype.constructor // true 上面代码表明,类的数据类型就是函数,类本身就指向构造函数。 使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。 class Bar { doStuff() { console.log('stuff'); } } const b = new Bar(); b.doStuff() // "stuff" 构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。 ...

October 2, 2024

Map与Set

每当去学习ES6的时候,我都会不由自主的打开阮一峰老师的《ES6入门》去学习和查找用法,相信大多数同学也都和我一样看过阮老师写的文章。 当然大家也都知道ES6里常用的API就那么些,不多不少,用在项目中也是刚刚好。 不过在每次读到Set和Map数据结构那一章的时候,总是有点不知所措,因为我不明白实现这样的数据结构,目的是什么,意义又是什么呢 Set和Map主要的应用场景在于数组去重和数据存储,幸运的是在读了关于数据结构和算法之类的书籍后,恍然大悟的发现 原来Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构 那么下面就来了解下这两种数据结构,最后来亲手实现的一个ES6中的Set和Map吧 集合 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组 ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值 Set 本身是一个构造函数,用来生成 Set 数据结构 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法 const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); // 2 3 5 4 } // 去除数组的重复成员 let array = [1,2,1,4,5,3]; [...new Set(array)] // [1, 2, 4, 5, 3] 具体用法如果还有不清楚的,这里我会在后面一一细说。现在还是来看一下以ES6中Set类(数据结构)为基础实现的集合吧 Set实例的属性和方法 Set的属性: size:返回集合所包含元素的数量 Set的方法: 操作方法 add(value):向集合添加一个新的项 delete(value):从集合中移除一个值 has(value):如果值在集合中存在,返回true,否则false clear(): 移除集合里所有的项 遍历方法 keys():返回一个包含集合中所有键的数组 values():返回一个包含集合中所有值的数组 entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了) forEach():用于对集合成员执行某种操作,没有返回值 创建一个集合 function Set(arr = []) { // 可以传入数组 let items = {}; this.size = 0; // 记录集合中成员的数量 } module.exports = Set; 这里用{}对象来表示集合,也是因为对象不允许一个键指向两个不同的属性,保证了集合里的元素都是唯一的 ...

October 2, 2024

WeakMap 和 WeakSet

1. 什么是 WeakSet WeakSet 结构与 Set 类似,也是不重复的值的集合,具备 WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。 WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。 WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 这三个方法。但是,它与 Set 有两个区别。 区别一:WeakSet 的成员只能是对象和Symbol类型,而不能是其他类型的值,我们用代码演示一下 从代码中可以看到,想像Set一样直接往 WeakSet 中添加原始类型化会报错,只能添加对象和Symbol 区别二(这是核心): WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 (这句解释来自ECMAScript 6 入门,第一眼看到这句话这话会觉得有点晦涩,我们来解释一下) 弱引用:垃圾回收机制有一套自己的回收算法,我们都知道一个函数执行完成后该函数在调用栈中创建的执行上下文会被销毁,这里说的销毁,其实指的就是执行上下文中环境变量、词法变量中的数据存储所占据的内存空间被垃圾回收机制所回收,那么垃圾回收机制不考虑 WeakSet 对该对象的引用是不是就意味着垃圾回收机制不会回收 WeakSet 对象里面的数据所占据的内存呢?不!不是的!代码是最好的解释 我们用 ws 中存放一个对象,然后再将该对象置为null,(这里要说明一下,一个变量被置为null,就意味着这个变量的内存可以被回收了)看着这个打印结果有没有突然明白了点什么,对!没错!只要 WeakSet 结构中的对象不再需要被引用,那么 WeakSet 就直接为空了,这不就意味着WeakSet中的数据所占据的内存被释放了吗。好的,你或许还会有疑问,难道不用 WeakSet 存储数据,结果就不是这样的吗,来打消你的疑虑 不使用 WeakSet 存放数据,当变量obj为null时,fistName依旧是有值的,对比这段代码,我们可以清晰的看出, WeakSet中 - 垃圾回收机制会自动回收该对象所占用的内存 2. 这里有’BUG' 可能已经有小伙伴拔出了他40米长的大剑了,上图二中的代码其实有问题,真实的打印应该是 各位手中的长剑收一收,先别急,其实我们前面聊的没有问题,为什么这里真是打印能出来结果呢?这个问题在国外某技术论坛上也是一度出现在 WeakSet 话题榜首位,解释为:因为浏览器的垃圾回收机制是不受我们控制的,我们无法知道垃圾回收机制什么时候执行,之所以打印能出现内容,其实是因为打印的时候垃圾回收机制没有启动。所以我们可以认为,上图二中的打印在垃圾回收启动之后,它就是空的 我们可以这样来证明它: 如果这样你觉得依旧不够睡服你自己的话,我们把这段代码搬到node中: ...

October 2, 2024

defineProperty 与 proxy

前言 我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢? definePropety ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 语法 Object.defineProperty(obj, prop, descriptor) 参数 obj: 要在其上定义属性的对象。 prop: 要定义或修改的属性的名称。 descriptor: 将被定义或修改的属性的描述符。 举个例子: var obj = {}; Object.defineProperty(obj, "num", { value : 1, writable : true, enumerable : true, configurable : true }); // 对象 obj 拥有属性 num,值为 1 虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。 函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。 两者均具有以下两种键值: configurable 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。 enumerable 当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值: value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 writable 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。 存取描述符同时具有以下可选键值: get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。 set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。 值得注意的是: ...

October 2, 2024

Reflect

一、Reflect有什么用? 一句话,Reflect没什么用,除了装装逼,让人看起来高大上以外,并不具有什么牛逼之处。 准确讲应该是这样的,Reflect更像是一种语法变体,其挂在的所有方法都能找到对应的原始语法,也就是Reflect的替代性非常强。 其实从Reflect这个单词本身字面意思就能体会出Reflect的神韵,Reflect的中文意思是“反射”,阳光照在镜子上反射,其实光子还是那些光子,只是变化了方向。 举例说明: Reflect对象挂载了很多静态方法,所谓静态方法,就是和Math.round()这样,不需要new就可以直接使用的方法。 比较常用的两个方法就是get()和set()方法: Reflect.get(target, propertyKey\[, receiver\]) Reflect.set(target, propertyKey, value\[, receiver\]) 就作用而言,等同于: target[propertyKey] target[propertyKey] = value; 比方说页面上有个输入框,其DOM对象变量是input,平时我们对整个输入框赋值使用的语句多半是: input.value = 'zhangxinxu'; 就可以直接使用Reflect.set()方法代替: Reflect.set(input, 'value', 'zhangxinxu') 效果是一模一样的。 又例如,我们希望对input的value属性重新定义,使该输入框value属性发生变化的时候可以同时触发'change'事件,下面是使用大家普遍比较熟悉的Object.defineProperty()方法实现的示意: const props = Object.getOwnPropertyDescriptor(input, 'value'); Object.defineProperty(input, 'value', { ...props, set (v) { let oldv = this.value; props.set.call(this, v); // 手动触发change事件 if (oldv !== v) { this.dispatchEvent(new CustomEvent('change')); } } }); 上述代码我们完全可以使用Reflect对象实现,具体的JavaScript代码如下所示。 const props = Reflect.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); Reflect.defineProperty(input, 'value', { ...props, set (v) { let oldv = this.value; props.set.call(this, v); // 手动触发change事件 if (oldv !== v) { this.dispatchEvent(new CustomEvent('change')); } } }); 我们可以测试下,假设页面HTML如下: <input id="input"> 测试代码为: input.addEventListener('change', () => { document.body.append('变化啦~'); }); input.value = 'zhangxinxu'; 此时,就可以看到页面上出现了“变化啦~”文字 ...

October 2, 2024

数组扩展

ES6(ECMAScript 2015)为JavaScript数组引入了多项扩展功能,这些功能不仅简化了数组操作,还提高了代码的可读性和效率。本文将详细介绍ES6数组扩展的主要功能和用法。 一、扩展运算符(…) 扩展运算符可以用于数组的赋值、合并、解构赋值以及将字符串转为数组。 赋值数组: let arr1 = [100, 200, 300, [500]]; let arr2 = [...arr1]; // 浅拷贝 console.log(arr2 == arr1); // false arr1[0] = 400; console.log(arr2); // 不受影响 arr1[3][0] = 600; console.log(arr2); // 受影响(只拷贝了一层) 合并数组: console.log([...[1, 2, 3], ...[4, 5, 6]]); // [1, 2, 3, 4, 5, 6] 解构赋值: let [a, ...list] = [1, ...["a", "b"]]; console.log(a); // 1 console.log(list); // ["a", "b"] 将字符串转为数组: console.log([..."str"]); // ["s", "t", "r"] 二、Array.from() Array.from()方法用于将类数组对象或可迭代对象转换为真正的数组。 let arrLike = { "0": "内容", "1": "内容2", "2": "内容3", length: 3 }; console.log(Array.from(arrLike)); // ["内容", "内容2", "内容3"] 此外,Array.from()还可以接受一个回调函数,用于对数组中的每个元素进行处理。 let arr = Array.from({ length: 2 }, () => "jack"); console.log(arr); // ["jack", "jack"] 三、Array.of() Array.of()方法用于将一组值转换为数组,与Array构造函数的行为不同,Array.of()不会把一个参数作为数组的长度。 ...

October 2, 2024

尾部优化

ES6 引入了尾调用优化(Tail Call Optimization,TCO)的规范,但它的实现依赖于 JavaScript 引擎的支持(比如 V8 并未开启 TCO),所以这是一个理论上重要、实践中需要谨慎对待的考点。 一、什么是尾调用(Tail Call)? 尾调用是指: 函数的最后一步是调用另一个函数,并直接返回这个函数的执行结果。 例如: function foo() { return bar(); // bar 是 foo 的尾调用 } 尾调用意味着: 当前函数的调用栈帧不需要再做任何事。 调用栈可以复用,不需要为当前函数保留栈帧,从而节省内存。 二、什么是尾调用优化(TCO)? 尾调用优化的目标是: 当一个函数的最后一步是调用另一个函数,JS 引擎可以不再保留当前函数的调用记录,直接跳转执行被调用函数,复用当前栈帧。 也就是说: 不会形成函数调用嵌套的“栈帧链”; 可以避免函数递归导致的栈溢出(stack overflow)。 三、尾调用成立的条件 必须同时满足以下三个条件: 条件 说明 1. 在严格模式下 TCO 只在 strict mode 下才可能被引擎开启 2. 是函数的最后一步调用 return 后没有任何操作 3. 返回调用结果 return bar(x) 形式,不能是 bar(x); return y ✅ 尾调用示例: 'use strict'; function f(x) { return g(x); // 最后一步是返回 g(x),符合尾调用优化条件 } ❌ 非尾调用: function f(x) { const y = g(x); return y; // 不是直接返回函数调用 } 四、尾调用优化应用场景:尾递归 递归调用时,若能做到尾调用,就能避免栈溢出。 ...

October 2, 2024

ES6 面试题

共 51 道 ES6 面试题。答案默认折叠,便于先自行作答。 1. 说说 Class 语法糖的底层实现 难度:3 · 类型:QA 题目要点 class 是基于函数构造器和原型链的语法封装,实例方法本质挂载在 prototype 上,继承通过 Object.create 构建原型链,同时通过构造函数原型链继承静态方法。super 依赖内部 [[HomeObject]] 机制定位父类方法。class 并未引入新的对象模型,而是在原型继承基础上增加语义约束与语法增强。 参考答案 这个问题的关键在于理解:class 并不是一种新的面向对象机制,而是对 原型继承(prototype-based inheritance) 的语法封装。底层仍然是函数构造器 + 原型链。 如果把 class 当作 Java 那种基于类的模型去理解,就会产生误解。 先看一个简单例子: class Person { constructor(name) { this.name = name; } say() { console.log(this.name); } static create(name) { return new Person(name); } } 它在底层大致等价于: function Person(name) { this.name = name; } Person.prototype.say = function () { console.log(this.name); }; Person.create = function (name) { return new Person(name); }; 一、实例方法的实现 ...