三种实例化对象的方式

两种形式定义类

工厂函数

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() { // 每个对象都有的toString函数
return `[${this.from}, ${this.to}]`
}
}

let r = range(1, 3) // 用r接住了一个使用工厂函数创建好的对象
console.log(r.includes(2)) // => true
console.log(r.toString()) // => [1, 3]
console.log([...r]) // =>[1,2,3]

构造函数

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()) // => [1, 3]
console.log(r.includes(2)) // => true
console.log([...r]) // => [1, 2, 3]

趁机回顾一下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()) // => [1, 3]
console.log(r.includes(2)) // => true
console.log([...r]) // => [1, 2, 3]

原型对象是类的关键特性,构造函数是类的公共标识

先熟悉一下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) // 9
↓↓↓
let Square = class {
constructor(x) {
this.area = x * x
}
}
new Square(3).area // 9

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)') // TypeError: parse is not a function 这样调用大错特错



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() // => hi~raliz
p.saying() // 114514
p.#qvq // 无法访问
p.updateSaying('555') // 114514->555

Person.getCnt() // 1
Person.cnt // 1
let p2 = new Person('qzhihe', 21)
Person.getCnt() // 2

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 // undefiend,不可访问
p.saying() // => 114514
Person.cnt // 1

为已有的类添加方法

JS基于原型机制是动态的,对象从它的原型继承属性,更改该原型中的属性后,对象会继承更改后的属性。

基于这一点,给已有的类增加方法不算难了↓

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对比新旧版本熟悉一下
// 旧的,nooooooooot goooooooooood 重名冲突、for/in可见
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}]`
}
}

// Range子类Span构造函数
function Span(start, span) {
if (span >= 0) {
this.from = start
this.to = start + span
} else {
this.from = start + span
this.to = start
}
}
// 确保Span的原型继承Range原型------关键一脚------!
Span.prototype = Object.create(Range.prototype)
// constructor指向构造函数
Span.prototype.constructor = Span
// 如果不想继承原型的toString,那就定义覆盖成自己的toString
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
// 创建一个Array的子类
class EZArray extends Array {
// 添加了两种获取函数
get first() {
return this[0]
}
get last() {
return this[this.length - 1]
}
}
let a = new EZArray()

a instanceof EZArray // true
a instanceof Array // true
a.push(1, 2)
a.pop() // 2
Array.isArray(a) // true
EZArray.isArray(a) // true
Array.prototype.isPrototypeOf(EZArray.prototype) // true
Array.isPrototypeOf(EZArray) // true

整一个检查键值类型的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}`)
}

// 调用父类的set方法,确保类型检查通过的键值对能加进去
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() {
// 创建一个要委托的Map对象
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
/**
* @param {number[]} nums
* @return {number}
*/

// 这个优先队列类看得我瞳孔扩散
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://leetcode.cn/problems/minimum-operations-to-halve-array-sum/solutions/1352690/zhou-sai-t3-by-lianxin-n8pt/