👍100个前端面试题及答案汇总
算法-冒泡排序及优化
JavaScript 中的私有变量
最近 JavaScript 有了很多改进,新的语法和功能一直在被增加进来。但有些东西并没有改变,一切仍然是对象,几乎所有东西都可以在运行时被改变,并且没有公共、私有属性的概念。但是我们自己可以用一些技巧来改变这种情况,在这篇文章中,我介绍各种可以实现私有变量的方式。
在 2015 年,JavaScript 有了 [类] ,对于那些从 更传统的 C 语系语言(如 Java 和 C#)过来的程序员们,他们会更熟悉这种操作对象的方式。但是很明显,这些类不像你习惯的那样 – 它的属性没有修饰符来控制访问,并且所有属性都需要在函数中定义。
那么我们如何才能保护那些不应该在运行时被改变的数据呢?我们来看看一些选项。
在整篇文章中,我将反复用到一个用于构建形状的示例类。它的宽度和高度只能在初始化时设置,提供一个属性来获取面积。有关这些示例中使用的
get
关键字的更多信息,请参阅我之前的文章 Getters 和 Setters。
命名约定
第一个也是最成熟的方法是使用特定的命名约定来表示属性应该被视为私有。通常以下划线作为属性名称的前缀(例如 _count
)。这并没有真正阻止变量被访问或修改,而是依赖于开发者之间的相互理解,认为这个变量应该被视为限制访问。1
2
3
4
5
6
7
8
9
10
11
12
13class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square._width); // 10
WeakMap
想要稍有一些限制性,您可以使用 WeakMap 来存储所有私有值。这仍然不会阻止对数据的访问,但它将私有值与用户可操作的对象分开。对于这种技术,我们将 WeakMap 的关键字设置为私有属性所属对象的实例,并且我们使用一个函数(我们称之为 internal
)来创建或返回一个对象,所有的属性将被存储在其中。这种技术的好处是在遍历属性时或者在执行 JSON.stringify
时不会展示出实例的私有属性,但它依赖于一个放在类外面的可以访问和操作的 WeakMap 变量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const map = new WeakMap();
// 创建一个在每个实例中存储私有变量的对象
const internal = obj => {
if (!map.has(obj)) {
map.set(obj, {});
}
return map.get(obj);
}
class Shape {
constructor(width, height) {
internal(this).width = width;
internal(this).height = height;
}
get area() {
return internal(this).width * internal(this).height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(map.get(square)); // { height: 100, width: 100 }
Symbol
Symbol 的实现方式与 WeakMap 十分相近。在这里,我们可以使用 Symbol 作为 key 的方式创建实例上的属性。这可以防止该属性在遍历或使用 JSON.stringify
时可见。不过这种技术需要为每个私有属性创建一个 Symbol。如果您在类外可以访问该 Symbol,那你还是可以拿到这个私有属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');
class Shape {
constructor(width, height) {
this[widthSymbol] = width;
this[heightSymbol] = height;
}
get area() {
return this[widthSymbol] * this[heightSymbol];
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.widthSymbol); // undefined
console.log(square[widthSymbol]); // 10
闭包
到目前为止所显示的所有技术仍然允许从类外访问私有属性,闭包为我们提供了一种解决方法。如果您愿意,可以将闭包与 WeakMap 或 Symbol 一起使用,但这种方法也可以与标准 JavaScript 对象一起使用。闭包背后的想法是将数据封装在调用时创建的函数作用域内,但是从内部返回函数的结果,从而使这一作用域无法从外部访问。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
}
get area() {
return this$.width * this$.height;
}
}
return new Shape(...arguments);
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
这种技术存在一个小问题,我们现在存在两个不同的 Shape
对象。代码将调用外部的 Shape
并与之交互,但返回的实例将是内部的 Shape
。这在大多数情况下可能不是什么大问题,但会导致 square instanceof Shape
表达式返回 false
,这可能会成为代码中的问题所在。
解决这一问题的方法是将外部的 Shape 设置为返回实例的原型:1
return Object.setPrototypeOf(new Shape(...arguments), this);
不幸的是,这还不够,只更新这一行现在会将 square.area
视为未定义。这是由于 get
关键字在幕后工作的缘故。我们可以通过在构造函数中手动指定 getter 来解决这个问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
Object.defineProperty(this, 'area', {
get: function() {
return this$.width * this$.height;
}
});
}
}
return Object.setPrototypeOf(new Shape(...arguments), this);
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
console.log(square instanceof Shape); // true
或者,我们可以将 this
设置为实例原型的原型,这样我们就可以同时使用 instanceof
和 get
。在下面的例子中,我们有一个原型链 Object -> 外部的 Shape -> 内部的 Shape 原型 -> 内部的 Shape
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function Shape() {
// 私有变量集
const this$ = {};
class Shape {
constructor(width, height) {
this$.width = width;
this$.height = height;
}
get area() {
return this$.width * this$.height;
}
}
const instance = new Shape(...arguments);
Object.setPrototypeOf(Object.getPrototypeOf(instance), this);
return instance;
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square.width); // undefined
console.log(square instanceof Shape); // true
Proxy
Proxy 是 JavaScript 中一项美妙的新功能,它将允许你有效地将对象包装在名为 Proxy 的对象中,并拦截与该对象的所有交互。我们将使用 Proxy 并遵照上面的 命名约定
来创建私有变量,但可以让这些私有变量在类外部访问受限。
Proxy 可以拦截许多不同类型的交互,但我们要关注的是 get
和 set
,Proxy 允许我们分别拦截对一个属性的读取和写入操作。创建 Proxy 时,你将提供两个参数,第一个是您打算包裹的实例,第二个是您定义的希望拦截不同方法的 “处理器” 对象。
我们的处理器将会看起来像是这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
};
在每种情况下,我们都会检查被访问的属性的名称是否以下划线开头,如果是的话我们就抛出一个错误从而阻止对它的访问。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
29class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
}
}
const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
square._width = 200; // 错误:试图访问私有属性
正如你在这个例子中看到的那样,我们保留使用 instanceof
的能力,也就不会出现一些意想不到的结果。
不幸的是,当我们尝试执行 JSON.stringify
时会出现问题,因为它试图对私有属性进行格式化。为了解决这个问题,我们需要重写 toJSON
函数来仅返回“公共的”属性。我们可以通过更新我们的 get 处理器来处理 toJSON
的特定情况:
注:这将覆盖任何自定义的
toJSON
函数。
1 | get: function(target, key) { |
我们现在已经封闭了我们的私有属性,而预计的功能仍然存在,唯一的警告是我们的私有属性仍然可被遍历。for(const key in square)
会列出 _width
和 _height
。谢天谢地,这里也提供一个处理器!我们也可以拦截对 getOwnPropertyDescriptor
的调用并操作我们的私有属性的输出:1
2
3
4
5
6
7getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
现在我们把所有特性都放在一起: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
48class Shape {
constructor(width, height) {
this._width = width;
this._height = height;
}
get area() {
return this._width * this._height;
}
}
const handler = {
get: function(target, key) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
} else if (key === 'toJSON') {
const obj = {};
for (const key in target) {
if (key[0] !== '_') {
obj[key] = target[key];
}
}
return () => obj;
}
return target[key];
},
set: function(target, key, value) {
if (key[0] === '_') {
throw new Error('Attempt to access private property');
}
target[key] = value;
},
getOwnPropertyDescriptor(target, key) {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (key[0] === '_') {
desc.enumerable = false;
}
return desc;
}
}
const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
console.log(JSON.stringify(square)); // "{}"
for (const key in square) { // No output
console.log(key);
}
square._width = 200; // 错误:试图访问私有属性
Proxy 是现阶段我在 JavaScript 中最喜欢的用于创建私有属性的方法。这种类是以老派 JS 开发人员熟悉的方式构建的,因此可以通过将它们包装在相同的 Proxy 处理器来兼容旧的现有代码。
附:TypeScript 中的处理方式
TypeScript 是 JavaScript 的一个超集,它会编译为原生 JavaScript 用在生产环境。允许指定私有的、公共的或受保护的属性是 TypeScript 的特性之一。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Shape {
private width;
private height;
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
}
const square = new Shape(10, 10)
console.log(square.area); // 100
使用 TypeScript 需要注意的重要一点是,它只有在 编译 时才获知这些类型,而私有、公共修饰符在编译时才有效果。如果你尝试访问 square.width
,你会发现,居然是可以的。只不过 TypeScript 会在编译时给你报出一个错误,但不会停止它的编译。1
2// 编译时错误:属性 ‘width’ 是私有的,只能在 ‘Shape’ 类中访问。
console.log(square.width); // 10
TypeScript 不会自作聪明,不会做任何的事情来尝试阻止代码在运行时访问私有属性。我只把它列在这里,也是让大家意识到它并不能直接解决问题。
未来
我已经向大家介绍了现在可以使用的方法,但未来呢?事实上,未来看起来很有趣。目前有一个提案,向 JavaScript 的类中引入 private fields,它使用 #
符号表示它是私有的。它的使用方式与命名约定技术非常类似,但对变量访问提供了实际的限制。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Shape {
#height;
#width;
constructor(width, height) {
this.#width = width;
this.#height = height;
}
get area() {
return this.#width * this.#height;
}
}
const square = new Shape(10, 10);
console.log(square.area); // 100
console.log(square instanceof Shape); // true
console.log(square.#width); // 错误:私有属性只能在类中访问
1. 实现一个call函数
1 | // 将要改变this指向的方法挂到目标this上执行并返回Function.prototype.mycall = function (context) { |
2. 实现一个apply函数
1 | Function.prototype.myapply = function (context) { |
3. 实现一个bind函数
1 | Function.prototype.mybind = function (context) { |
4. instanceof的原理
1 | // 右边变量的原型存在于左边变量的原型链上 |
5. Object.create的基本实现原理
1 | functioncreate(obj) { |
6. new本质
1 | function myNew (fun) { |
7. 实现一个基本的Promise
1 | // ①自动执行函数,②三个状态,③then |
8. 实现浅拷贝
1 | // 1. ...实现let copy1 = {...{x:1}} |
9. 实现一个基本的深拷贝
1 | // 1. JOSN.stringify()/JSON.parse() |
10. 使用setTimeout模拟setInterval
1 | // 可避免setInterval因执行时间导致的间隔执行时间不一致 |
11. js实现一个继承方法
1 | // 借用构造函数继承实例属性 |
12. 实现一个基本的Event Bus
1 | // 组件通信,一个触发与监听的过程 |
13. 实现一个双向数据绑定
1 | let obj = {} |
完整实现可前往之前写的:这应该是最详细的响应式系统讲解了
14. 实现一个简单路由
1 | class Route{ |
15. 实现懒加载
1 | <ul><li><imgsrc="./imgs/default.png"data="./imgs/1.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/2.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/3.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/4.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/5.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/6.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/7.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/8.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/9.png"alt=""></li><li><imgsrc="./imgs/default.png"data="./imgs/10.png"alt=""></li></ul> |
16. rem实现原理
1 | functionsetRem () { |
17. 手写实现AJAX
1 | // 1. 简单实现// 实例化let xhr = new XMLHttpRequest() |
18. 实现拖拽
1 | window.onload = function () { |
19. 实现一个节流函数
1 | functionthrottle (fn, delay) { |
20. 实现一个防抖函数
1 | functiondebounce (fn, delay) { |