任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别。
构造函数
通过构造函数创建的每一个实例都会自动将构造函数的 prototype 属性作为其[[Prototype]]
。即:
Object.getPrototypeOf(new Box()) === Box.prototype。
Constructor.prototype
默认具有一个自有属性:constructor,它引用了构造函数本身。即,Box.prototype.constructor === Box
。这允许我们在任何实例中访问原始构造函数。
继承
- 用 extends 关键字来声明这个类继承自另一个类。
- 如果子类有任何自己的初始化内容需要完成,它也必须先使用
super()
来调用父类的构造函数,并传递父类构造函数期望的任何参数。 - 我们还覆盖了父类的
introduceSelf()
方法
JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 中,任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
class Professor extends Person {
teaches;
constructor(name, teaches) {
super(name);
this.teaches = teaches;
}
introduceSelf() {
console.log(
`My name is ${this.name}, and I will be your ${this.teaches} professor.`,
);
}
grade(paper) {
const grade = Math.floor(Math.random() * (5 - 1) + 1);
console.log(grade);
}
}
封装
私有数据属性必须在类的声明中声明,而且其名称需以 #
开头。
与私有数据属性一样,你也可以声明私有方法。而且名称也是以 #
开头,只能在类自己的方法中调用。
class Student extends Person {
#year;
constructor(name, year) {
super(name);
this.#year = year;
}
#somePrivateMethod() {
console.log("You called me?");
}
introduceSelf() {
console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
}
canStudyArchery() {
return this.#year > 1;
}
}
原型和原型链
一定区分两个概念:
.prototype
属性是与函数绑定的概念,只在函数被用作构造函数时活跃.[[prototype]]
或Object.getPrototypeOf()
或.__proto__
访问的是每个对象都有的 原型- 联系:通过构造函数
new
实例时,构造函数的prototype
会成为实例的[[prototype]]
`
每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象.
指向对象原型的属性并不是 prototype。它的名字不是标准的,但实际上所有浏览器都使用 __proto__
。访问对象原型的标准方法是 Object.getPrototypeOf()。
它不应与函数的 .prototype
属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象实例的 [[Prototype]]
。我们将在后面的小节中讨论构造函数的原型属性。
在 JavaScript 中,所有的函数都有一个名为 prototype
的属性(指向一个对象)。当你调用一个函数作为构造函数时,这个属性被设置为新构造对象的原型(按照惯例,在名为 __proto__
的属性中)。
设置对象的原型
Object.setPrototypeOf()
静态方法可以将一个指定对象的原型(即内部的[[Prototype]]
属性)设置为另一个对象或者null
。Object.create()
方法创建一个新的对象,并允许你指定一个将被用作新对象原型的对象。- 设置一个构造函数的
prototype
(用Object.assign()
函数),我们可以确保所有用该构造函数创建的对象都被赋予该原型 - 也可以直接
func.prototype.methodName = function(){}
直接修改构造函数的 prototype 对象中的单个属性
const personPrototype = {
greet() {
console.log("hello!");
},
};
const carl = Object.create(personPrototype);
carl.greet(); // hello!
// ====================================================
const personPrototype = {
greet() {
console.log(`你好,我的名字是 ${this.name}!`);
},
};
function Person(name) {
this.name = name;
}
Object.assign(Person.prototype, personPrototype);
// 或
// Person.prototype.greet = personPrototype.greet;
自有属性
我们经常看到这种模式,即方法是在原型上定义的,但数据属性是在构造函数中定义的。这是因为方法通常对我们创建的每个对象都是一样的,而我们通常希望每个对象的数据属性都有自己的值(就像这里每个人都有不同的名字)。

直接在对象中定义的属性,如这里的 name
,被称为自有属性,你可以使用静态方法 Object.hasOwn()
检查一个属性是否是自有属性
const irma = new Person("Irma");
console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false
new 的过程
当使用 new 关键字调用函数时,该函数将被用作构造函数。new 将执行以下操作:
- 创建一个空的简单 JavaScript 对象。为方便起见,我们称之为 newInstance。
- 如果构造函数的
prototype
属性是一个对象,则将 newInstance 的[[Prototype]]
指向构造函数的prototype
属性,否则 newInstance 将保持为一个普通对象,其[[Prototype]]
为Object.prototype
。
TIP
因此,通过构造函数创建的所有实例都可以访问添加到构造函数 prototype
属性中的属性/对象。
- 使用给定参数执行构造函数,并将 newInstance 绑定为
this
的上下文(换句话说,在构造函数中的所有this
引用都指向 newInstance)。 - 如果构造函数返回非原始值,则该返回值成为整个
new
表达式的结果。否则,如果构造函数未返回任何值或返回了一个原始值,则返回 newInstance。(通常构造函数不返回值,但可以选择返回值,以覆盖正常的对象创建过程。)
为什么了解原型链
了解原型继承模型是使用它编写复杂代码的重要基础。此外,要注意代码中原型链的长度,在必要时可以将其分解,以避免潜在的性能问题。
DANGER
此外,除非是为了与新的 JavaScript 特性兼容,否则永远不应扩展原生原型。
性能:<查找属性>原型链上较深层的属性的查找时间可能会对性能产生负面影响,这在性能至关重要的代码中可能会非常明显。此外,尝试访问不存在的属性始终会遍历整个原型链。
在遍历对象的属性时,原型链中的每个可枚举属性都将被枚举。要检查对象是否具有在其自身上定义的属性,而不是在其原型链上的某个地方,则有必要使用 hasOwnProperty
或 Object.hasOwn
方法。
除[[Prototype]]
为 null
的对象外,所有对象都从 Object.prototype
继承 hasOwnProperty
——除非它已经在原型链的更深处被覆盖。
闭包 Clousure
概念
首先注意 MDN 中的这一段对闭包的说明:
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
其中,内部函数访问外部函数的作用域其实是所有函数都能做到的,因为在 JavaScript 中,访问一个变量时会依次查找作用域链上的所有作用域。也就是说,内部函数默认就是能够访问外部作用域的变量的。
我把这种现象称为广义上的闭包现象,而相对的狭义上的闭包现象,key insight 是函数维持了一个对外层作用域变量的引用。
具体来说,一个函数所在的执行上下文已经退出(Call Stack),但是该函数仍然能访问到外层作用域本应随着执行上下文退出而被回收的变量。形象地说,这个函数将外部作用域的变量装进了一个背包中并随身携带。
例子
js
const calcSum = function (a, b) {
return a + b
}
function addOne(num) {
const one = 1
return function inner() {
return calcSum(num, one)
}
}
const resultOfOnePlusTwo = addOne(2)
console.log(resultOfOnePlusTwo())
在这个例子中,为了给 resultOfOnePlusTwo
赋值,需要执行 addOne
函数,可以通过调试工具观察到其作用域链和调用栈:

TIP
脚本作用域中没有 addOne
是因为这个函数声明被放到了 global 作用域中,也就是 window 对象中。这类似通过 var
声明的变量成为全局对象的属性。
继续执行到 console.log
这一行前,这时 resultOfOnePlusTwo
已经赋值,值为对 inner
函数实例的引用,此时该实例已经带上了“背包”:

当通过 resultOfOnePlusTwo
调用 inner
函数时,可以看到作用域中出现了闭包:

总结
主要观察狭义上的闭包现象。
一般来说执行上下文退出后就会回收其中的变量,但是这里 addOne
已经运行结束并退出执行上下文了但是引用 resultOfOnePlusTwo
仍然能通过 inner
访问到这个函数作用域中的变量。
其本质是在返回这个 inner
实例的引用时,inner
通过身上的私有变量 Scopes
记录了它需要使用的外部作用域的变量,也就是闭包(不是所有外部作用域的变量都被保存,只有被返回的这个函数需要访问的变量)。当调用 inner
时,就可以在这个闭包作用域中找到自己需要的变量。
编译设计
这种设计的优雅之处在于把闭包当作一个作用域放进了函数的私有变量 Scopes
中,当函数被调用时仍然通过通用的查找作用域链的方式就可以获取到这种特殊的变量。
应用场景
还是比较直观的可以发现闭包的应用,主要是一些需要返回函数的场景。
现在 js 也支持类的私有属性,感觉也可以用类实现。
- 防抖函数
工厂模式生成添加了防抖功能的事件回调函数。
js
const debounce = function (fn, delay, immediate = false) {
let timer = null
let isInvoke = false
// 这个函数作为事件处理函数被绑定到元素上,触发时会接收到参数,一般是事件对象 event
// 用 ...args 捕获传入的参数装到 args 数组(Array)中
// 也可以不声明这个 ...args,用 arguments[0] 访问到这个事件参数 event
const _debounce = function (...args) {
if (timer) {
// 取消之前的定时
clearTimeout(timer)
}
if (immediate && !isInvoke) {
fn.apply(this, args)
// 在下一次执行 fn 函数之前,都不会立即执行,也就是不会进入这个 if
isInvoke = true
// immediate = false 如果这样写,以后都不会再立即执行
}
else {
// 设置一个新的定时器,如果这个函数再次被触发,会导致这个定时器被取消
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
// 当走这个 else statement 的 fn 执行后,可以再次进入 then statement 分支
// 也就是说,在连续快速点击按钮的情况下,fn 触发的方式总是:立即 - 定时器 - 立即 - 定时器...
isInvoke = false
// 解除对定时器的引用,CG
timer = null
}
}
return _debounce
}
小心
这里执行 fn
函数,也就是执行需要进行防抖处理的函数,一定要用 apply
绑定 this
。或者使用自动寻找外部作用域 this
值的箭头函数
若是如此调用 fn(...args)
那就只是个普通的函数调用。在this的介绍中已经提到,函数作用域内的 this
是访问这个函数的对象,而 fn(...args)
并不是通过点表达式调用的,所以其中的 this
是全局对象。
如果在 fn
函数内部写 console.log(this)
在浏览器中调试,结果为 window 对象,作为佐证。
有的需求并不会用到 this 参数没有影响,但是有的回调会利用到对应的 this 上下文,所以务必绑定 this。这也是闭包返回函数时需要注意的问题。
- 节流函数
js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true }) {
// 1.记录上一次的开始时间
let lastTime = 0
const {leading} = options
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 当lastTime为0时且第一次不执行,此时
// remainTime = interval - (nowTime - lastTime) = interval > 0
if (!lastTime && !leading) lastTime = nowTime//决定第一次是否执行函数
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间,
// 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn.apply(this, args)
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
设计模式
命令模式 Command Pattern
Key Insights
- 将发起命令的主体和真正执行命令的主体解耦
- 具体构造结构
- 请求者类:发起命令的主体
- 命令类:符合特定接口的命令
- 接收者类:真正执行命令的主体
以下示例缺少命令执行者。
js
class OrderManager {
constructor() {
this.orders = [];
}
execute(command, ...args) {
return command.execute(this.orders, ...args);
}
}
class Command {
constructor(execute) {
this.execute = execute;
}
}
function PlaceOrderCommand(order, id) {
return new Command(orders => {
orders.push(id);
console.log(`You have successfully ordered ${order} (${id})`);
});
}
function CancelOrderCommand(id) {
return new Command(orders => {
orders = orders.filter(order => order.id !== id);
console.log(`You have canceled your order ${id}`);
});
}
function TrackOrderCommand(id) {
return new Command(() =>
console.log(`Your order ${id} will arrive in 20 minutes.`)
);
}
const manager = new OrderManager();
manager.execute(new PlaceOrderCommand("Pad Thai", "1234"));
manager.execute(new TrackOrderCommand("1234"));
manager.execute(new CancelOrderCommand("1234"));
观察者模式 Observer Pattern
一个可被观察对象,要能提供订阅和自动通知两个功能。
js
class Observable {
constructor() {
// 需要被触发的函数列表
this.observers = []
}
subscribe (func) {
this.observers.push(func)
}
unsubscribe (func) {
this.observers = this.observers.filter((item) => item !== func)
}
/**
* @param data 通知信息
*/
notify (data) {
this.observers.forEach((observer) => observer(data))
}
}
箭头函数
箭头函数表达式的语法比传统的函数表达式更简洁,但在语义上有一些差异,在用法上也有一些限制:
- 箭头函数没有独立的
this
、arguments
和super
绑定,并且不可被用作方法。- 箭头函数不能用作构造函数。使用 new 调用它们会引发 TypeError。它们也无法访问 new.target 关键字。
- 箭头函数不能在其主体中使用 yield,也不能作为生成器函数创建。
- 箭头函数没有独立的
this
、arguments
和super
绑定
箭头函数没有自己的 this
,所以箭头函数里面的 this 在定义它时就确定了,并且不会改变,就是其所在上下文的 this
.(关于 this 所在的上下文)
也没有一般函数对象都有的 arguments
类数组局部变量。
箭头函数不能用作构造函数,当然也没有 prototype
属性(__proto__
是有的!),所以用于访问自身原型的 super
属性也没有。
INFO
箭头函数在其周围的作用域上创建一个 this
值的闭包,这意味着箭头函数的行为就像它们是“自动绑定”的——无论如何调用,this
都绑定到函数创建时的值(在上面的例子中,是全局对象)。在其他函数内部创建的箭头函数也是如此:它们的 this
值保持为闭合词法上下文的 this
.
- 箭头函数不可被用作方法
也就是说箭头函数不能成为 Object 的属性。即使将箭头函数赋值给一个对象的属性,调用这个函数也不会根据实例的不同而发生改变。因为 this 不变,当箭头函数成为方法,每一次调用构造函数实际上就创建了一个闭包。因为这个箭头函数始终能访问构造函数的变量。
方法是一种作为对象的属性的函数。方法有两种类型:实例方法是由对象实例执行的内置任务,而静态方法是直接在对象构造函数上调用的任务。
备注:在 JavaScript 中,函数本身也是对象,因此,在这个上下文中,方法实际上是对函数的对象引用。
- 箭头函数不能用作生成器
箭头函数的主体中不能用 yield 关键字。
小语法八股
作用域
INFO
作用域是当前的执行上下文,在其中的值和表达式“可见”(可被访问)。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
JavaScript 的作用域分以下三种:
- 全局作用域:脚本模式运行所有代码的默认作用域
- 模块作用域:模块模式中运行代码的作用域
- 函数作用域:由函数创建的作用域
此外,用 let 或 const 声明的变量属于额外的作用域:
- 块级作用域:用一对花括号(一个代码块)创建出来的作用域
Key Insight:JS 的作用域是静态的,也就是所谓的词法作用域,函数的作用域是由它在代码中的位置决定的。这意味着函数的作用域在函数定义时就已确定,而不是在函数调用时确定。
比如,下面这个例子中只需要看 foo()
函数定义(而不是调用)外层有没有 value
,值是多少,不用管它在哪里被调用。
js
let value = 1
function foo() { // 函数签名
console.log(value)
}
function bar () {
let value = 2
foo.apply(this)
}
bar() // 输出 1
fetch API
js
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Array.prototype.map() & filter() & reduce()
map
: 将数组映射为一个新数组,浅拷贝
filter
: 过滤数据,浅拷贝
reduce
: 对数组应用一个函数,上一个元素apply这个函数的返回值,会成为下一次调用这个函数的的输入accumulator
参数值
js
const arr = [1, 2, 3, 4, 5]
const sumvalue = arr.reduce((accumulator, currentvalue, index, array) => {
return accumulator + currentvalue
}, 0)
console.log(sumvalue) // 15
this
在非严格模式下,this 总是指向一个对象,在严格模式下可以是任意值。有关如何确定该值的更多信息,请参阅下面的描述。
在非严格模式下,如果一个函数被调用时其 this
值不是一个对象,那么 this
值会被替换为一个对象。null
和 undefined
会变成 globalThis
。像 7
或 'foo'
这样的原始值会使用相关的构造函数转换为对象,所以原始数值 7
会被转换为一个 Number
包装类,字符串 'foo'
会被转换为一个 String
包装类。
TIP
this 的值取决于它出现的上下文:函数、类或全局。
在函数上下文中,this
= 访问该函数的对象。
当一个函数作为回调函数传递时,this
的值取决于如何调用回调,这由 API 的实现者决定。回调函数通常以 undefined
作为 this
的值被调用(直接调用,而不附加到任何对象上),这意味着如果函数是在非严格模式,this
的值会是全局对象(globalThis)。这在迭代数组方法、Promise()
构造函数等例子中都是适用的。
TIP
forEach
方法用迭代器获取数组的值,并把他们一一放到函数中执行,所以并没有 1.logThis()
这样使用回调函数,而是 logThis(1)
直接调用,此时 this
当然是非严格的 globalThis
或是严格的 undefined
.
js
function logThis() {
"use strict";
console.log(this);
}
[1, 2, 3].forEach(logThis); // undefined、undefined、undefined
当然很多 API 都提供了传入 this 参数的方法:
js
// Array.prototype.forEach(callback, thisArg)
[1, 2, 3].forEach(logThis, { name: "obj" });
// { name: 'obj' }, { name: 'obj' }, { name: 'obj' }
退出数组的迭代方法循环
以下以forEach()
方法为例,说明所谓迭代方法的原理。
forEach()
方法是通用的。它只期望 this
值具有 length
属性和整数键的属性。
INFO
一般来说,以下对象可以使用该方法遍历:
- 数组 (Array)
- Typed Arrays – 类型化数组
- Int8Array
- Unit8Array
- Float32Array
- NodeList – 类数组
- querySelectorAll 返回值
- 其他需要使用 Array.from() 方法转换为数组,或者用 Array.prototype.forEach.call(obj, func)调用以指定 this 参数
诸如 forEach()
, map()
, reduce()
, every()
等这些数组的迭代方法,其实现是类似的。 Array实例的[Symbol.iterator]()
方法实现了可迭代协议。迭代方法通过调用该方法获取到数组迭代器对象,然后通过迭代器获取数组中的值,将其应用到回调函数中。
退出 forEach 的循环:
INFO
由迭代方法的原理不难看出为什么不能在回调函数中用 break
退出循环:break
作用在回调函数内部,而不是循环中。
要退出循环意味着要中止函数的执行。
- 抛出异常
throw Error
(要配合 try…catch…语法使用) - 在循环中改变数组,使其迭代器停止
- array.length = 0
- array.splice() 截断数组后面
WARNING
第二种方式会改变数组形态,注意。
js
// 第一种方式
try {
arr.forEach((item) => {
if (item === 2) {
throw Error('stop!')
}
})
}catch(err) {
// 可以是空
}
console.log('continue')
Array.isArray() 实现
在 ECMAScript 规范中,Array.isArray()
的实现如下:
js
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
这里用 call()
方法指定了 this
参数为传入方法的数组实例。
为什么不用 array.toString()
方法?因为 Array.prototype
中的 toString()
方法覆盖了原型链上祖先的同名方法。
Array.isArray() 的实现原理
在 JavaScript 中,所有的对象都有一个内部属性
[[Class]]
,这个属性可以通过Object.prototype.toString()
方法来访问。
对于数组对象,Object.prototype.toString()
方法返回 "[object Array]"
。基于这一特性,可以实现一个功能等同于 Array.isArray()
的方法。
js 数据类型
js 中的原始值被分为7种基本类型:
- 7个基本类型(放在栈中):Number,String,Undefined,Null,Boolean,BigInt,Symbol
- 1个引用类型(放在堆中,GC干活出来干活!): Object
Map 数据结构
插入小技巧:统计数组中元素出现的次数,有则+1,无则加入
js
let map = new Map();
nums.forEach(num => {
let cunt = map.get(num) || 0;
map.set(num, ++cunt);
})
看看 Map 里面键值对的结构
js
new Map([
['foo', 3],
['bar', {}],
['baz', undefined],
])
Map 实现了迭代器,所以可以用展开运算符。
Map.prototype.keys()
返回的也是一个迭代器对象,同样可以展开:
js
let sortedMap = new Map([...map].sort((a, b) => b[1] - a[1]))
let keys = [...sortedMap.keys()]
Set
Set 对象允许你存储任何类型(无论是原始值还是对象引用)的唯一值。 Set 对象是值的合集(collection)。集合(set)中的元素只会出现一次,即集合中的元素是唯一的。你可以按照插入顺序迭代集合中的元素。插入顺序对应于 add() 方法成功将每一个元素插入到集合中(即,调用 add() 方法时集合中不存在相同的元素)的顺序。
创建一个 Set 对象,使用其构造方法
js
new Set()
new Set(iterable)
如果传入一个可迭代对象,它的所有元素将不重复地被添加到新的 Set 中。如果不指定此参数或其值为 null,则新的 Set 为空。
JSON.stringify()
基本类型的转换参考如下:
基本类型
- 字符串(String): 转换为不带引号的字符串值。 特殊字符(如双引号、反斜杠和行终止符)会被转义。
- 数字(Number): 转换为不带引号的数字文本。 NaN 和 Infinity 被转换为 null。
- 布尔值(Boolean): true 转换为字符串 “true”。 false 转换为字符串 “false”。
- null: 转换为字符串 “null”。
- 其他: undefined、函数以及 Symbol 值在数组中会被转换为 null,而在非数组对象中会被忽略。
主要观察一些特定的对象类型,因为应用比较多,比如对象数组去重(用 Set 去重)。
对象(Object):
- 转换为 JSON 对象文本。
- 仅包含可枚举的自有属性。
- 属性的键必须是字符串,非字符串键会被转换为字符串。
- 如果属性值是 undefined、函数或 Symbol,则会被忽略。
- 如果对象包含循环引用,会抛出错误。
数组(Array):
- 转换为 JSON 数组文本。
- 保留数组中的元素顺序。
- undefined、函数和 Symbol 值会被转换为 null。
遍历对象的属性
有非常多种方法可以遍历对象的不同特点的一组属性。据个人观察一般需求只访问对象本身的可枚举属性,就用 keys()
方法来遍历即可。
具体对比如下表所示。
方法名 | 原型链 | 属性 | 备注 |
---|---|---|---|
for...in 循环 | 是 | 可枚举属性 | 可以用于对象和数组,但通常不推荐用于数组 |
obj.keys() | 否 | 可枚举属性 | 只遍历对象本身的属性,不受原型链影响 |
obj.getOwnPropertyNames() | 否 | 所有属性,包括不可枚举 | 不遍历原型链 |
obj.getOwnPropertySymbols() | 否 | 符号属性 | 用于遍历符号属性 |
Reflect.ownKeys(obj) | 否 | 所有属性,包括字符串、符号和不可枚举的属性 | 最全面的属性遍历方法 |
0 条评论