三种实例化对象的方式
两种形式定义类
工厂函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function range(from, to) { let r = Object.create(range.methods) r.from = from r.to = to return r } range.methods = { includes(x) { return x >= this.from && x <= this.to }, *[Symbol.iterator]() { for (let i = Math.ceil(this.from); i <= this.to; i ++) { yield i } }, toString() { return `[${this.from}, ${this.to}]` } }
let r = range(1, 3) console.log(r.includes(2)) console.log(r.toString()) console.log([...r])
|
构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function Range(from, to) { this.from = from this.to = to } Range.prototype = { includes: function(x) { return x >= this.from && x <= this.to }, [Symbol.iterator]: function*() { for (let i = Math.ceil(this.from); i <= this.to; i++) { yield i } } , toString: function() { return `[${this.from}, ${this.to}]` } }
let r = new Range(1, 3) console.log(r.toString()) console.log(r.includes(2)) console.log([...r])
|
趁机回顾一下new 一个新对象的过程
- 创建一个空对象
__proto__
指向构造函数的prototype
this
指向该对象
趁机回顾一下构造函数和普通函数调用的区别
- this:构造函数通过new调用,this指向新建的空对象(指向实例对象);而普通函数调用this往往参照所在上下文,一般是window,反正谁调用这个函数,这个函数的this就指向谁。
- 返回值:构造函数不需要显示地返回值,调用的时候自动返回一个实例对象;普通函数必须要有返回值,且返回值取决于函数内部的return语句,不然undefined。
一种ES6船新的class关键字类
实际上是语法糖啦~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Range { constructor(from, to) { this.from = from this.to = to } includes(x) { return x >= this.from && x <= this.to } *[Symbol.iterator]() { for (let i = Math.ceil(this.from); i <= this.to; i++) { yield i } } toString() { return `[${this.from}, ${this.to}]` } }
let r = new Range(1, 3) console.log(r.toString()) console.log(r.includes(2)) console.log([...r])
|
原型对象是类的关键特性,构造函数是类的公共标识
先熟悉一下class
继承类语法
extends咯
1 2 3 4 5 6 7 8 9
| class Span extends Range { constructor(start, length) { if (length >= 0) { super(start, start + length) } else { super(start + length, start) } } }
|
类定义表达式语法
这种情况蛮少,还是上面的比较舒服
1 2 3 4 5 6 7 8 9 10 11
| let square = function(x) { return x * x } square(3) ↓↓↓ let Square = class { constructor(x) { this.area = x * x } } new Square(3).area
|
class声明
类声明不会像函数声明一样有声明提升。必须先声明再初始化操作。
class声明体内
声明体内所有代码默认处于严格模式下,也就是说不能使用八进制整数字面量、with语句啥啥啥的,那当然如果你在声明一个变量前使用了这个变量,g咯。
静态方法
所谓静态方法,是作为构造函数而非原型对象的属性定义的——静态方法是构造函数的方法。
静态方法也被称为’类方法’,因为要通过类名/构造函数名调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Range { .... static parse(s) { let matches = s.matches(/^\(\d+)\.\.\.(\d+)\$/) if (!matches) { throw new TypeError('NoNo') } return new Range(parseInt(matches[1]), parseInt(matches[2])) } }
let r = Range.parse('(1...10)') r.parse('(1...10)')
function MyClass(....) {....}
MyClass.staticMethod = function() { console.log('构造函数中的静态方法') }
MyClass.staticMethod()
|
在静态方法中使用this没啥意义,因为本身就是靠类/构造函数调用,而非实例对象。
获取、设置等方法
对象字面量支持的所有简写的方法定义语法都可以在类体中使用。且包括一些特殊的方法定义,比如生成器方法,名字为方括号中表达式值的方法等
公有、私有、静态字段
- public fields 公有字段 在类中直接声明的属性,可以在类的实例中访问,包含各种数据类型
- privite fields 私有字段 只能在类内部访问的属性,对外部不可见,前有
#
标识符,一般用来放类里敏感数据/要隐藏的细节的
- static fileds 静态字段 属于类本身的属性,在所有类实例间共享,前有
static
关键字,一般用来类级别上存储状态/计数
ES6 class风格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Person { constructor(name, age) { this.name = name this.age = age } sayHi() { console.log(`hi~${this.name}`) } #qvq = '114514' saying() { return this.#qvq } updateSaying(s) { this.#qvq = s } static cnt = 0 calculator() { Person.cnt++ } static getCnt() { return Person.cnt } }
let p = new Person('raliz', 21) p.sayHi() p.saying() p.#qvq p.updateSaying('555')
Person.getCnt() Person.cnt let p2 = new Person('qzhihe', 21) Person.getCnt()
|
ES5 构造函数风格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function Person(name, age) { this.name = name this.age = age let qvq = '114514' this.saying = function() { console.log(this.qvq) } Person.cnt++ } Person.prototype.sayHi = function() { cosole.log(`hi~${this.name}`) }
Person.cnt = 0
let p = new Person('raliz', 21) p.qvq p.saying() Person.cnt
|
为已有的类添加方法
JS基于原型机制是动态的,对象从它的原型继承属性,更改该原型中的属性后,对象会继承更改后的属性。
基于这一点,给已有的类增加方法不算难了↓
1 2 3 4 5 6 7 8 9 10 11 12 13
|
if (!String.prototype.startsWith) { String.prototype.satrtsWith = function(s) { return this.indexOf(s) === 0 } }
class ExternP extends Person { newMethod() { console.log('添加一个新方法') } }
|
子类
子类和原型
让我们来用构造函数的方式实现一个Range的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function Range(from, to) { this.from = from this.to = to } Range.prototype = { includes: function(x) { return x >= this.from && x <= this.to }, [Symbol.iterator]: function*() { for (let i = Math.ceil(this.from); i <= this.to; i++) { yield i } } , toString: function() { return `[${this.from}, ${this.to}]` } }
function Span(start, span) { if (span >= 0) { this.from = start this.to = start + span } else { this.from = start + span this.to = start } }
Span.prototype = Object.create(Range.prototype)
Span.prototype.constructor = Span
Span.prototype.toString = function() { return `哈哈[${this.from}, ${this.to}]哈哈` }
|
创建子类
通过extends和super创建子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class EZArray extends Array { get first() { return this[0] } get last() { return this[this.length - 1] } } let a = new EZArray()
a instanceof EZArray a instanceof Array a.push(1, 2) a.pop() Array.isArray(a) EZArray.isArray(a) Array.prototype.isPrototypeOf(EZArray.prototype) Array.isPrototypeOf(EZArray)
|
整一个检查键值类型的Map子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class TypedMap extends Map { constructor(keyType, valType, entries) { if (entries) { for (let [k, v] of entries) { if (typeof k !== keyType || typeof v !== valType) { throw new TypeError(`存在不符合类型限制的键值对[${k}, ${v}]`) } } } super(entries) this.keyType = keyType this.valType = valType } set(key, val) { if (keyType && typeof key !== this.keyType) { throw new TypeError(`存在不符合类型限制的键${key}`) } if (valType && typeof val !== this.valType) { throw new TypeError(`存在不符合类型限制的值${val}`) } return super.set(key, val) } }
|
在构造函数中使用Super需要注意的规则
- 如果使用extends继承一个类,那么必须要使用super来初始化调用一下父类构造函数
- 如果子类中没有写构造函数,那么解释器会自动创建一个空构造函数,隐式地获取传值和调用
- 在调用super()之前,不能在构造函数中初始化子类状态(使用this关键字),这种强制规则确保了父类先于子类得到初始化
- 当子类构造函数被调用且super调用父类构造函数时,父类构造函数可以通过new.target获取到子类构造函数。
- 没有new调用的函数中,new.target是undefiend;构造函数中new.target引用的是被调用的构造函数。
- 一个设计良好的父类不需要直到自己的子类,不过可以通过new.target.name来记录日志消息。
- ↑根据单一职责原则,开方封闭原则,依赖倒置原则,低耦合,重用,抽象…父类不应依赖于子类,so↑
委托
能组合不继承
如果有个类和另一个类有相同的行为,可以通过创建子类来继承该行为,但是在类中创建另一个类的实例,并在需要的时候委托该实例去做希望做的事情反而是更方便灵活。也就是说这时候不需要创建子类,而是包装/组合其他类即可。
以上这种委托策略 被称为’组合composition’。
实现一个直方图类↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Histogram { ocnstructor() { 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) { this.map.set(key, this.count(key) + 1) } delete(key) { let cnt = this.count(key) if (cnt === 1) { this.map.delete(key) } else { this.map.set(key, cnt - 1) } } [Symbol.iterator](){ return this.map.keys() } keys() { return this.map.keys() } values() { return this.map.keys() } entries() { return this.map.entries() } }
|
正式的继承关系虽好,可不要贪杯哦(doge)
类层次和抽象类
我觉得和设计模式结合来看比较好。
一道算法题
2208. 将数组和减半的最少操作次数
给你一个正整数数组 nums
。每一次操作中,你可以从 nums
中选择 任意 一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续执行操作)
请你返回将 nums
数组和 至少 减少一半的 最少 操作数。
示例 1:
1 2 3 4 5 6 7 8 9 10 11
| 输入:nums = [5,19,8,1] 输出:3 解释:初始 nums 的和为 5 + 19 + 8 + 1 = 33 。 以下是将数组和减少至少一半的一种方法: 选择数字 19 并减小为 9.5 。 选择数字 9.5 并减小为 4.75 。 选择数字 8 并减小为 4 。 最终数组为 [5, 4.75, 4, 1] ,和为 5 + 4.75 + 4 + 1 = 14.75 。 nums 的和减小了 33 - 14.75 = 18.25 ,减小的部分超过了初始数组和的一半,18.25 >= 33/2 = 16.5 。 我们需要 3 个操作实现题目要求,所以返回 3 。 可以证明,无法通过少于 3 个操作使数组和减少至少一半。
|
示例 2:
1 2 3 4 5 6 7 8 9 10 11
| 输入:nums = [3,8,20] 输出:3 解释:初始 nums 的和为 3 + 8 + 20 = 31 。 以下是将数组和减少至少一半的一种方法: 选择数字 20 并减小为 10 。 选择数字 10 并减小为 5 。 选择数字 3 并减小为 1.5 。 最终数组为 [1.5, 8, 5] ,和为 1.5 + 8 + 5 = 14.5 。 nums 的和减小了 31 - 14.5 = 16.5 ,减小的部分超过了初始数组和的一半, 16.5 >= 31/2 = 16.5 。 我们需要 3 个操作实现题目要求,所以返回 3 。 可以证明,无法通过少于 3 个操作使数组和减少至少一半。
|
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 107
解题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
class Heap { constructor(data, compare) { this.data = data; this.compare = compare;
for (let i = (data.length >> 1) - 1; i >=0 ; i--) { this.heapify(i); } } heapify(index) { let target = index; let left = index * 2 + 1; let right = index * 2 + 2; if (left < this.data.length && this.compare(this.data[left], this.data[target])) { target = left; } if (right < this.data.length && this.compare(this.data[right], this.data[target])) { target = right; } if (target !== index) { this.swap(target, index); this.heapify(target); } } swap(l, r) { let data = this.data; [data[l], data[r]] = [data[r], data[l]]; } push(item) { this.data.push(item); let index = this.data.length - 1; let father = ((index + 1) >> 1) - 1; while (father >= 0) { if (this.compare(this.data[index], this.data[father])) { this.swap(index, father); index = father; father = ((index + 1) >> 1) - 1; } else { break; } } } pop() { this.swap(0, this.data.length - 1); let ret = this.data.pop(); this.heapify(0); return ret; } }
const halveArray = function(nums) { let pq = new Heap(nums, (lower, higher) => { return lower > higher; }); let total = nums.reduce((total, item) => total + item); let now = 0; let times = 0; while (now < total / 2) { let tmp = pq.pop(); now += tmp / 2; pq.push(tmp / 2); times++; } return times; };
题解链接:https:
|