Skip to main content

Javascript 基础

1、 对象字面量

1.1、 对象字面量语法

JS 中创建一个自定义对象有两种方法,一种是使用new,另一种是使用对象字面量形式

var person={
name:'小王',
age:18,
_pri:233
}

1.2、 对象成员配置

  • configurable:是否可以删除某个成员,默认为true
  • writable:成员是否可写,默认为true
  • enumerable:成员是否可被枚举,默认为true
    • 该属性主要是用来防范Object.keys()和for in的
  • get与set:读写成员时调用的函数,默认为undefined

1.3、 对象保护

  • 禁止添加成员:Object.preventExtensions()该方法用于阻止向对象添加成员,使用Object.isExtensible()判断对象是否可添加成员
  • 禁止添加、删除成员:Object.seal()用来阻止添加或删除成员,判断对象是否是密封的可采用Object.isSealed()
  • 禁止任何操作:使用Object.freeze()方法后,除了不能添加删除成员,连成员的赋值都会失效

1.4、 继承

Object.create(person)可产生一个具有继承特性的新对象,但是需要注意的是,父对象的引用类型成员是和子对象共享的,当子对象修改引用类型的成员时,父对象的该成员也会同步发生变化

var person={
name:'小王',
age:18,
_pri:233,
gf:['豆得儿','张G','TFb']
}
var child=Object.create(person);

2、 变量提升

变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值为 undefined

正是由于 JavaScript 存在变量提升这种特性,导致了很多与直觉不太相符的代码,这也是 JavaScript 的一个设计缺陷。

导致变量提升的原因?

变量提升和 JavaScript 的编译过程密切相关:JavaScript 和其他语言一样,都要经历编译和执行阶段。在这个短暂的编译阶段,JS 引擎会搜集所有的变量声明,并且提前让声明生效。而剩下的语句需要等到执行阶段、等到执行到具体的某一句时才会生效

2.1、 let 和 const 的由来

ECMAScript6 已经通过引入块级作用域并配合使用 let、const 关键字,可以避开变量提升的设计缺陷。

但是由于 JavaScript 需要向下兼容,所以变量提升在很长时间内还会继续存在。

2.2、 var 提升变量

在 ECMAScript6 之前,JS 引擎用 var 关键字声明变量。

console.log(a);
var a = 1;
function b() {}
// output:
// undefined

2.3、 函数提升

函数提升跟 var 定义的变量提升有点区别,var 定义的,变量提升初始化赋值是 undefined。

函数提升是相当于把函数移动到了上下文的顶部。

console.log(fn);
function fn() {}
// output:
// f fn() {}

2.4、 var定义的函数

考虑下如下代码,我们经常这样写 inline function 对吧。

function callback() {
setPageColor();
var setPageColor = function () {
document.body.backgroundColor = '#ccc';
};
}

callback();

// output:
// Uncaught TypeError: setPageColor is not a function

2.5、 作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在 ES6 之前,作用域分为两种:

  • 全局作用域 中的对象在代码中的任何地方都可以访问,其生命周期伴随着页面的生命周期。
  • 函数作用域 是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

ES6 开始支持块级作用域了。

哪些语法会产生块级作用域?

if (boolean) {
// block scope
}

在 ES6 中,以下语法会创建块级作用域:

  • if 语句
  • switch 语句
  • for 语句
  • while 语句
  • do...while 语句
  • try...catch 语句
  • with 语句
  • 函数内部

2.6、 暂时性死区

什么是暂时性死区?

在 let 定的变量之前提前使用了变量,就是暂时性死区。

看如下代码。

var name = 'JavaScript';
{
name = 'CSS';
let name;
}
// output:
// Uncaught ReferenceError: Cannot access 'name' before initialization

其实,这并不意味着引擎感知不到 name 变量的存在,恰恰相反,它感知到了,而且它清楚地知道 name 是用 let 声明在当前块里的。正因如此,它才会给这个变量加上暂时性死区的限制。一旦去掉 let 关键字,它也就不起作用了。

其实这也就是暂时性死区的本质:当程序的控制流程在新的作用域进行实例化时,在此作用域中用 let 或者 const 声明的变量会先在作用域中被创建出来,但此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这段时间,就称之为暂时死区。

在 let 和 const关键字出现之前,typeof运算符是百分之百安全的,现在也会引发暂时性死区的发生,像import关键字引入公共模块、使用new class创建类的方式,也会引发暂时性死区,究其原因还是变量的声明先与使用。

typeof a    // Uncaught ReferenceError: a is not defined
let a = 1

可以看到,在a声明之前使用typeof关键字报错了,这就是暂时性死区导致的。


3、 对象成员私有化

3.1、 使用闭包

闭包是 JavaScript 中的一个重要概念,它指的是函数可以访问其外部作用域中的变量。可以使用闭包来实现对象的私有成员。

例如,以下代码使用闭包来实现对象的私有成员:

function Person(name) {
const _name = name;

this.getName = function () {
return _name;
};

this.setName = function (newName) {
_name = newName;
};
}

const person = new Person("John Doe");

console.log(person.getName()); // "John Doe"

person.setName("Jane Doe");

console.log(person.getName()); // "Jane Doe"

该代码首先定义一个 Person 类。该类有一个私有成员 _name,用于存储对象的姓名。该类还提供两个公有方法 getName 和 setName,用于获取和设置对象的姓名。

由于 _name 变量是私有的,因此只能在 Person 类的构造函数和方法中访问。这可以防止外部代码意外更改对象的姓名。

3.2、 使用 Symbol

Symbol 是 ES6 中引入的一种新数据类型。它可以用于创建唯一标识符。可以使用 Symbol 来实现对象的私有成员。

例如,以下代码使用 Symbol 来实现对象的私有成员:

const _name = Symbol();

class Person {
constructor(name) {
this[_name] = name;
}

getName() {
return this[_name];
}

setName(newName) {
this[_name] = newName;
}
}

const person = new Person("John Doe");

console.log(person.getName()); // "John Doe"

person.setName("Jane Doe");

console.log(person.getName()); // "Jane Doe"

该代码首先定义一个 Symbol 值 _name。该值是唯一的,因此无法在外部代码中访问。然后,该代码定义一个 Person 类。该类有一个私有成员 _name,用于存储对象的姓名。该类还提供两个公有方法 getName 和 setName,用于获取和设置对象的姓名。

由于 _name 变量是 Symbol 值,因此只能在 Person 类的构造函数和方法中访问。这可以防止外部代码意外更改对象的姓名。

3.3、 使用 class 的私有成员

ES2022 引入了 class 的私有成员。使用 class 的私有成员可以更轻松地实现对象的私有化。

例如,以下代码使用 class 的私有成员来实现对象的私有成员:

class Person {
#name = "John Doe";

constructor(name) {
this.#name = name;
}

getName() {
return this.#name;
}

setName(newName) {
this.#name = newName;
}
}

const person = new Person("John Doe");

console.log(person.getName()); // "John Doe"

person.setName("Jane Doe");

console.log(person.getName()); // "Jane Doe"

该代码首先定义一个 Person 类。该类有一个私有成员 #name,用于存储对象的姓名。该类还提供两个公有方法 getName 和 setName,用于获取和设置对象的姓名。

由于 #name 变量是私有的,因此只能在 Person 类的构造函数和方法中访问。这可以防止外部代码意外更改对象的姓名。

在实际应用中,可以根据需要选择使用上述方法来实现对象的私有化。


4、 Object 对比 Map

区别如下

特性ObjectMap
键的类型字符串、数字、Symbol任意类型
键值对的顺序不确定插入时的顺序
迭代for...in、Object.keys()for...of、Map.prototype.entries()
性能一般
其他原生对象、可用于创建对象字面量ES6 新对象、不可用于创建对象字面量

5、 map 和 set 的区别

  • Map 是一种键值对的集合,每个键值对由一个键和一个值组成。键可以是任何类型的数据,而值可以是任何类型的数据。Map 的特点是键必须唯一,也就是说,不能有两个相同的键存在于同一个 Map 中。
  • Set 是一种无序集合,其中每个元素只能出现一次。Set 的特点是元素必须唯一,也就是说,不能有两个相同的值存在于同一个 Set 中。

存储方式

  • Map 存储键值对,而 Set 存储单个元素。
  • Map 的键必须唯一,而 Set 中的元素必须唯一。

用法

  • Map 用于存储需要根据键快速查找值的场景,例如,缓存、对象属性等。
  • Set 用于存储需要快速判断元素是否唯一存在的情况,例如,集合去重、检查元素是否存在等。

6、 对象的遍历方式

  • for...in 循环是最常用的方法,但它可能会遍历继承的属性。
  • Object.keys() 方法只遍历对象自身的属性,并且返回一个数组,方便后续操作。
  • forEach() 方法可以对每个元素执行回调函数,更灵活。
  • Object.values() 方法只返回属性值,方便取
  • for...of 循环可以遍历可迭代对象,包括数组、对象和字符串
object-iterator.js
// for of
for (const [key, value] of obj) {
console.log(key, value);
}

// Object.keys
const keys = Object.keys(obj);
for (const key of keys) {
console.log(key, obj[key]);
}

// for in
const obj = {
name: '张三',
age: 30,
city: '北京'
};

for (const key in obj) {
console.log(key, obj[key]);
}

// forEach
obj.forEach((value, key) => {
console.log(key, value);
});

// Object.values
const values = Object.values(obj);
for (const value of values) {
console.log(value);
}


7、 JS 面向对象

JavaScript 既是面向对象语言,也是基于原型的语言。它支持面向对象的三个要素:封装、继承和多态。

7.1、 封装

JavaScript 可以使用对象来封装数据和方法,对外暴露接口。

const person = {
name: "张三",
age: 18,
sayHello() {
console.log("Hello, my name is Zhang San.");
},
};

person.sayHello(); // Hello, my name is Zhang San.

7.2、 继承

JavaScript 可以使用原型链来实现继承。

const Person = function (name, age) {
this.name = name;
this.age = age;
};

Person.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name);
};

const student = new Person("李四", 20);

student.sayHello(); // Hello, my name is 李四

7.3、 多态

JavaScript 可以通过重写方法来实现多态。

const Person = function (name, age) {
this.name = name;
this.age = age;
};

Person.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name);
};

const Student = function (name, age, school) {
Person.call(this, name, age);
this.school = school;
};

Student.prototype = Object.create(Person.prototype);

Student.prototype.sayHello = function () {
console.log("Hello, my name is " + this.name + ", and I study at " + this.school);
};

const student = new Student("王五", 22, "清华大学");

student.sayHello(); // Hello, my name is 王五, and I study at 清华大学

7.4、 不足

  • 类不是语言的核心: JavaScript 的类只是一个语法糖,它不是语言的核心。在 JavaScript 中,可以使用对象来实现面向对象的编程,而不需要使用类。
  • 没有单一继承: JavaScript 只支持原型链继承,不支持单一继承。
  • 没有接口: JavaScript 没有接口,这使得代码难以复用。

8、 继承方式

JavaScript 中有以下几种常见的继承方式:

  • 原型链继承:这是 JavaScript 中最常用的继承方式,通过修改子类的原型对象来实现继承。
  • 构造函数继承:通过在子类的构造函数中调用父类的构造函数来实现继承。
  • 组合继承:结合原型链继承和构造函数继承的优点来实现继承。
  • 寄生继承:通过创建一个新的对象来继承父类的属性和方法。
  • 寄生组合继承:结合寄生继承和组合继承的优点来实现继承。

8.1、 原型链继承

原型链继承是 JavaScript 中最常用的继承方式,通过修改子类的原型对象来实现继承。

function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

function Child(name) {
// 继承 Parent 的原型
Child.prototype = Object.create(Parent.prototype);

this.name = name;
}

const child = new Child('John');
child.sayHello(); // 输出: Hello, my name is John

解释:

  • 在 Child 构造函数中,将 Parent 原型的副本赋值给 Child 原型。
  • 这样,子类就继承了父类的原型链。
  • 子类实例可以通过原型链访问父类原型上的属性和方法。

8.2、 构造函数继承

构造函数继承通过在子类的构造函数中调用父类的构造函数来实现继承。

function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

function Child(name) {
// 继承 Parent 的属性和方法
Parent.call(this, name);

this.age = age;
}

const child = new Child('John', 20);
child.sayHello(); // 输出: Hello, my name is John
console.log(child.age); // 输出: 20

解释:

  • 在 Child 构造函数中,调用 Parent 构造函数,并将 this 指向子类实例。
  • 这样,子类实例就继承了父类实例的所有属性和方法。

8.3、 组合继承

组合继承结合原型链继承和构造函数继承的优点来实现继承。

function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

function Child(name) {
// 继承 Parent 的原型
Child.prototype = Object.create(Parent.prototype);

// 继承 Parent 的属性和方法
Parent.call(this, name);

this.age = age;
}

const child = new Child('John', 20);
child.sayHello(); // 输出: Hello, my name is John
console.log(child.age); // 输出: 20

解释:

  • 组合继承结合了原型链继承和构造函数继承的优点。
  • 子类既可以继承父类的原型链,也可以继承父类实例的属性和方法。

8.4、 寄生继承

寄生继承通过创建一个新的对象来继承父类的属性和方法。

function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

function Child(name) {
const obj = new Parent(name);

// 继承 Parent 的属性和方法
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
this[key] = obj[key];
}
}

this.age = age;
}

const child = new Child('John', 20);
child.sayHello(); // 输出: Hello, my name is John
console.log(child.age); // 输出: 20

解释:

  • 寄生继承通过创建一个新的对象来继承父类的属性和方法。
  • 子类不会继承父类的原型链。

8.5、 寄生组合继承

寄生组合继承结合寄生继承和组合继承的优点来实现继承。

function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

function Child(name) {
// 创建一个新的对象来继承父类的属性和方法
const obj = new Parent(name);

// 继承 Parent 的原型
Child.prototype = Object.create(Parent.prototype);

// 将新对象中的属性和方法添加到子类原型中
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
Child.prototype[key] = obj[key];
}
}

// 子类实例的额外属性和方法
this.age = age;
}

const child = new Child('John', 20);
child.sayHello(); // 输出: Hello, my name is John
console.log(child.age); // 输出: 20

解释:

  • 寄生组合继承结合了寄生继承和组合继承的优点。
  • 子类既可以继承父类的原型链,也可以继承父类实例的属性和方法。
  • 同时,子类实例也可以拥有自己的额外属性和方法。

寄生组合继承的优点:

  • 可以避免原型链污染。
  • 可以实现多重继承。
  • 可以更加灵活地控制继承关系。

寄生组合继承的缺点:

  • 代码实现比较复杂。
  • 性能略差于原型链继承。

9、 闭包

闭包(Closure)是指能够访问另一个函数作用域中变量的函数。换句话说,闭包就是将函数内部和函数外部连接起来的一座桥梁

function createCounter() {
let counter = 0;
return function() {
counter++;
return counter;
};
}

const c1 = createCounter();
const c2 = createCounter();

console.log(c1()); // 1
console.log(c2()); // 1
console.log(c1()); // 2
console.log(c2()); // 2

在这个例子中,createCounter 函数返回的函数可以访问外部函数中的变量 counter,即使外部函数已经执行完毕。因此,c1 和 c2 两个函数可以独立地维护自己的计数器。

闭包会造成内存泄露:如果闭包中的变量一直不被使用,那么这些变量就会一直存在于内存中,造成内存泄露。因此,在使用闭包时,要注意释放不再使用的变量

10、 深拷贝

10.1、 浅拷贝有哪些

  • {...obj}
  • Object.assign({}, obj)

10.2、 JSON.stringify

该方法有以下特点:

  • 布尔值、数值、字符串对应的包装对象,在序列化过程会自动转换成其原始值。
  • undefined任意函数Symbol 值,在序列化过程有两种不同的情况。若出现在非数组对象的属性值中,会被忽略;若出现在数组中,会转换成 null
  • 任意函数undefined 被单独转换时,会返回 undefined
  • 所有以 Symbol 为属性键的属性都会被完全忽略,即便在该方法第二个参数 replacer 中指定了该属性。
  • Date 日期调用了其内置的 toJSON() 方法转换成字符串,因此会被当初字符串处理。
  • NaNInfinity 的数值及 null 都会当做 null
  • 这些对象 MapSetWeakMapWeakSet 仅会序列化可枚举的属性。
  • 被转换值如果含有 toJSON() 方法,该方法定义什么值将被序列化。
  • 对包含 循环引用 的对象进行序列化,会抛出错误。

10.3、 第三方库

jQuery.extendlodash.cloneDeep 都是深拷贝的实现。

10.4、 自己写递归

基础的

function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
// 基本数据类型或 null,直接返回
return obj;
}

// 数组类型的深拷贝
if (Array.isArray(obj)) {
return obj.slice();
}

// 普通对象的深拷贝
const newObj = {};
for (const key in obj) {
newObj[key] = deepCopy(obj[key]);
}
return newObj;
}

解决循环引用的情况

function deepCopy(obj, seen = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
// 基本数据类型或 null,直接返回
return obj;
}

// 如果对象已经被 seen 了,则直接返回引用
if (seen.has(obj)) {
return seen.get(obj);
}

// 数组类型的深拷贝
if (Array.isArray(obj)) {
const newObj = obj.slice();
seen.set(obj, newObj);
return newObj;
}

// 普通对象的深拷贝
const newObj = {};
seen.set(obj, newObj);
for (const key in obj) {
newObj[key] = deepCopy(obj[key], seen);
}
return newObj;
}

解决数组成员是引用类型的情况

function deepCopy(obj, seen = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
// 基本数据类型或 null,直接返回
return obj;
}

// 如果对象已经被 seen 了,则直接返回引用
if (seen.has(obj)) {
return seen.get(obj);
}

// 数组类型的深拷贝
if (Array.isArray(obj)) {
const newObj = [];
seen.set(obj, newObj);
for (let i = 0, len = obj.length; i < len; i++) {
newObj[i] = deepCopy(obj[i], seen);
}
return newObj;
}

// 普通对象的深拷贝
const newObj = {};
seen.set(obj, newObj);
for (const key in obj) {
newObj[key] = deepCopy(obj[key], seen);
}
return newObj;
}

如果对象类型是引用类型

function deepCopy(obj, seen = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
// 基本数据类型或 null,直接返回
return obj;
}

// 如果对象已经被 seen 了,则直接返回引用
if (seen.has(obj)) {
return seen.get(obj);
}

// 数组类型的深拷贝
if (Array.isArray(obj)) {
const newObj = [];
seen.set(obj, newObj);
for (let i = 0, len = obj.length; i < len; i++) {
newObj[i] = deepCopy(obj[i], seen);
}
return newObj;
}

// 普通对象的深拷贝
const newObj = {};
seen.set(obj, newObj);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 如果属性是日期对象,则创建新的日期对象
if (obj[key] instanceof Date) {
newObj[key] = new Date(obj[key].getTime());
} else {
newObj[key] = deepCopy(obj[key], seen);
}
}
}
return newObj;
}

11、 Proxy&Object.defineProperty 区别

方面ProxyObject.defineProperty
语法使用 new Proxy(target, handler) 创建代理对象直接在对象上使用 Object.defineProperty(obj, prop, descriptor)
监听属性变化支持监听整个对象的变化,通过 get 和 set 方法拦截只能监听单个属性的变化,通过 get 和 set 方法拦截
功能拦截可以拦截并重写多种操作,如 get、set、deleteProperty 等只能拦截属性的读取和赋值操作
可迭代性支持迭代器,可以使用 for...of、Array.from() 等进行迭代不支持迭代器,无法直接进行迭代操作
兼容性部分浏览器不支持,如 IE相对较好的兼容性,支持大多数现代浏览器
性能相对较低,因为每次操作都需要经过代理相对较高,因为直接在对象上进行操作
扩展性可以通过添加自定义的 handler 方法进行扩展不支持扩展,只能使用内置的 get 和 set 方法拦截