LeetCode / JavaScript No.04 计数器 II
🗿🗿🗿本系列文档在 LeetCode 30 天 JavaScript 挑战 完成后可看到题解,本人只转载方便自看
JavaScript 对象
在本质上,「对象」 只是从字符串到其他值的映射。这些值可以是任何类型:字符串、函数、其他对象等。将映射到值的字符串称为 「键」 。
const object = {
"num": 1,
"str": "Hello World",
"obj": {
"x": 5
}
};
有三种访问对象值的方式:
- 点符号表示法
const val = object.obj.x;
console.log(val); // 5
- 方括号表示法
当键不是有效的变量名时使用。例如".123"
const val = object["obj"]["x"];
console.log(val); // 5
- 解构语法
在一次访问多个值时非常有用。
const { num, str } = object;
console.log(num, str); // 1 "Hello World"
有关对象的更多信息,点击 这里 进行了解。
类和原型
可以在 JavaScript
中定义 类。类的构造函数返回一个对象,这个对象是该类的实例。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log("My name is", this.name);
}
}
const alice = new Person("Alice", 25);
alice.greet(); // 输出:"My name is Alice"
JavaScript
使用特殊对象 prototypes
来实现类。所有方法(在这个例子中是 greet
)都是存储在对象的原型上的函数。
为了更具体的说明,上面的操作可以使用以下代码实现:
const alice = {
name: "Alice",
age: 25,
__proto__: {
greet: function() {
console.log("My name is", this.name);
}
}
};
alice.greet(); // 输出:"My name is Alice"
- 为什么这里可以访问
greet
方法,即使它不是alice
对象上的键?
因为访问对象上的键实际上比仅查看对象的键稍微复杂一点。实际上有一个算法会遍历 「原型链」。首先,·JavaScript· 查看对象上的键。如果请求的键没有找到,它会查找原型对象的键。如果仍然没有找到,会查看原型的原型,以此类推。这就是 JavaScript
中 「继承」 的实现方式。
- 为什么
JavaScript
会有这种奇怪的原型概念。为什么不直接将函数存储在对象本身上?
因为效率。每次创建新的 Person
对象时,都会向对象添加 age
和 name
字段。然而,只会添加一个对原型对象的 「引用」。因此,无论创建多少个 Person
实例还是类上有多少方法,都只会生成一个原型对象。
(续)
继承和代码复用: 原型链允许对象通过原型继承属性和方法。这意味着在JavaScript中,可以创建一个对象,并将其原型指向另一个对象,从而实现属性和方法的共享。这种机制使得代码更具有可复用性,可以通过原型链来定义通用的功能,然后让其他对象共享这些功能。
动态性: JavaScript是一种动态语言,允许在运行时修改对象的结构。使用原型链,可以动态地为对象添加新的属性和方法,而不需要重新定义对象本身。
内存效率: 将方法存储在原型上,而不是每个实例对象上,可以节省内存空间。如果每个实例对象都有自己的方法,那么每个对象都需要额外的内存来存储这些方法,而使用原型链,方法只需要在原型对象上定义一次,然后所有实例对象都可以共享。
在 这里 阅读更多关于类的内容。
代理
JavaScript
中不经常使用但非常强大的功能之一是 「代理」。它们允许覆盖对象的默认行为。
例如,使用代理来实现 alice
对象:
const alice = new Proxy({ name: "Alice", age: 25 }, {
get: (target, key) => {
if (key === 'greet') {
return () => console.log("My name is", target.name);
} else {
return target[key];
}
},
});
alice.greet(); // 输出:"My name is Alice"
以下是一些代理的潜在实用案例示例:
- 执行验证以确保不会将不良数据输入表单。
const validator = {
set: (obj, prop, value) => {
if (prop === "age") {
if (typeof value !== "number" || value < 0) {
throw new TypeError("Age must be a positive number");
}
}
obj[prop] = value;
},
};
const person = new Proxy({}, validator);
person.age = 25; // 正常工作
person.age = -5; // 抛出异常
- 创建每次访问键时都记录的日志。这是非常有用的开发者工具。
const object = {
"num": 1,
"str": "Hello World",
"obj": {
"x": 5
}
};
const proxiedObject = new Proxy(object, {
get: (target, key) => {
console.log("Accessing", key);
return target[key];
}
});
proxiedObject.num; // 打印: Accessing num
- 如果尝试写入只读值,则引发错误。
const READONLY_KEYS = ['name'];
const person = new Proxy({ name: "Alice", age: 25 }, {
set: (target, key, value) => {
if (READONLY_KEYS.includes(key)) {
throw Error("Cannot write to key");
}
target[key] = value;
return true;
}
});
person.name = "Bob"; // 抛出异常
比如流行的 Immer库 内部使用了 JavaScript
的代理(Proxy
)来实现其核心功能。Immer 库利用了代理的能力来拦截对原始数据的操作,并根据这些操作生成不可变的数据版本。这种方式使得可以编写可变的代码,而实际上操作的是不可变的数据。
Immer 的核心概念就是创建一个代理对象,然后通过对代理对象的操作来生成不可变数据版本。这样做使得编写代码更加简洁和直观,而不必担心不可变性的细节。
在 这里 阅读更多关于代理的内容。
使用代理的闭包解决 计数器 II
与其返回一个普通对象,不如返回一个模拟具有方法的对象行为的代理 Proxy。可以通过监听所有属性访问(get
)事件,并且如果请求的键与方法的名称匹配,就执行相应的逻辑来实现这一点。
以下解决方案主要是为了演示。代理是一个非常强大的工具,应该在绝对需要的情况下再使用。
var createCounter = function(init) {
let currentCount = init;
return new Proxy({}, {
get: (target, key) => {
switch(key) {
case "increment":
return () => ++currentCount;
case "decrement":
return () => --currentCount;
case "reset":
return () => (currentCount = init);
default:
throw Error("Unexpected Method")
}
},
});
};