1. 说说JavaScript中的数据类型?
基本数据类型:
- Number:用于表示数字,可以是整数或浮点数,还包括
Infinity
、-Infinity
和NaN
(Not a Number)。 - String:表示文本数据,可以是一个字符或一串字符,使用单引号、双引号或反引号来定义。
- Boolean:有两个值,
true
和false
,用于进行逻辑操作。 - Undefined:当一个变量被声明了但没有被赋值时,它的值就是
undefined
。 - Null:表示一个空值,通常用于表示某个对象变量现在没有指向任何对象。
- Symbol(ES6 新增):表示独一无二的值,主要用于创建对象的唯一属性名。
- BigInt(近期新增):用于表示大于 2^53 - 1 的整数。
引用数据类型:
- Object:JavaScript 中的对象是键值对的集合。
- Array:用于表示元素的有序集合。
- Function:函数实际上是一种特殊类型的对象,可以被调用。
- Date:用于处理日期和时间。
- RegExp:用于执行正则表达式匹配。
2. 说说你对闭包的理解?闭包使用场景
我的理解是 就是函数包函数 里面的函数引用外层函数的变量的组合就是闭包
闭包是指有权访问另一个函数作用域中变量的函数
最简单的例子
function A() {
let count = 1
function B() {
count++
}
B()
}
使用场景
-
数据封装和信息隐藏:通过闭包,可以创建私有变量,避免全局变量的污染。
function createCounter() { let count = 0; return { increment: function() { count++; return count; }, decrement: function() { count--; return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.decrement()); // 1
-
在循环中创建局部作用域:解决循环中使用异步函数导致的变量作用域问题。
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, j * 1000); })(i); }
-
函数柯里化:通过闭包,可以实现函数的柯里化,预先设置一些参数。
function curriedAdd(a) {
return function (b) {
return a + b
}
}
const addFive = curriedAdd(5)
console.log(addFive(3)) // 输出:8
闭包的注意事项
虽然闭包非常有用,但它们也可能导致内存泄漏,因为闭包中的外部函数作用域的变量不会被垃圾回收器回收,直到闭包本身被销毁。因此,在使用闭包时应当注意不要过度使用,以避免占用过多内存。
3. JavaScript中的原型,原型链分别是什么?
一张图就能全部理解
__proto__
: 隐式原型,是每个对象都具有的属性
prototype
: 显式原型,是Function独有的属性
function Student(name, age) {
this.name = name;
this.age = age;
}
// 原型上挂载方法
Student.prototype.getName = function () {
return this.name;
};
const s1 = new Student("张三", 18); // 构造函数
// 这样的就可以访问到原型上的方法了
console.log("🚀 ~ s1:", s1.getName());
// 当实例对象,在自身找不到时,会向上一级原型(Student.prototype)对象上查找;
// 如果还找不到,则 又会到原型对象上一级的原型对象(Object.prototype)上查找,这种链式查找机制,称为原型链
4. Javascript如何实现继承?
在 JavaScript 中实现继承主要有几种方式:
原型链继承
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
// 子类构造函数
function Cat(name) {
this.name = name;
}
// 原型指向 Animal
Cat.prototype = new Animal();
// Cat就可以继承Animal的属性和方法了
var Cat1 = new Cat("Cat1");
Cat1.sleep(); // Cat1正在睡觉!
console.log(Cat1.name); // Cat1
console.log(Cat1.colors); // [ 'black', 'white' ]
var Cat2 = new Cat("Cat2");
// 因为两个实例使用的是同一个原型对象,内存空间是共享的
Cat2.colors.push("yellow");
console.log(Cat1.colors); // [ 'black', 'white', 'yellow' ]
console.log(Cat2.colors); // [ 'black', 'white', 'yellow' ]
构造函数继承
构造函数继承的关键在于,在子类构造函数中调用父类构造函数,并将子类实例的this
绑定到父类构造函数上,从而使得父类的属性成为子类实例的属性
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
Animal.prototype.do = function () {
console.log(this.name + "正在做动作!");
};
// 子类构造函数
function Cat(name) {
// 在子类构造函数中调用父类构造函数
Animal.call(this, name);
}
var Cat1 = new Cat("Cat1");
console.log(Cat1.name); // Cat1
console.log(Cat1.colors); // [ 'black', 'white' ]
// Cat1.do(); // 报错 cat.do is not a function
var Cat2 = new Cat("Cat2");
Cat2.colors.push("yellow");
console.log(Cat1.colors); // ["black", "white"];
console.log(Cat2.colors); // ["black", "white", "yellow"];
// 相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端
// 但是只能继承父类的实例属性和方法,不能继承原型属性或者方法
组合继承
结合了原型链继承和构造函数继承的优点,组合继承能够实现函数复用(通过原型链继承原型上的属性和方法)同时还能保证每个实例都有自己的属性(通过构造函数继承实例属性)
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
Animal.prototype.do = function () {
console.log(this.name + "正在做动作!");
};
// 子类构造函数
function Cat(name) {
// 在子类构造函数中调用父类构造函数
Animal.call(this, name);
}
// 原型链继承,继承父类的原型方法
Cat.prototype = new Animal();
var Cat1 = new Cat("Cat1");
Cat1.sleep(); // Cat1正在睡觉!
Cat1.do(); // Cat1正在做动作! 这样do方法可以被访问
var Cat2 = new Cat("Cat2");
Cat2.colors.push("yellow");
console.log(Cat1.colors); // [ 'black', 'white' ]
console.log(Cat2.colors); // [ 'black', 'white', 'yellow' ] 这样不会被共享
原型式继承
Object.create
方法实现普通对象的继承
var Animal = {
name: "动物",
colors: ["black", "white"],
sleep: function () {
console.log(this.name + "正在睡觉!");
},
};
var Cat = Object.create(Animal, { name: { value: "猫" } });
Cat.sleep(); // 猫正在睡觉!
var Dog = Object.create(Animal, { name: { value: "狗" } });
Dog.sleep(); // 狗正在睡觉!
Dog.colors.push("yellow");
console.log(Cat.colors); // [ 'black', 'white', 'yellow' ]
console.log(Dog.colors); // [ 'black', 'white', 'yellow' ] 和原型链继承一样共享内存
寄生式继承
寄生式继承是一种使用函数来增强对象的继承方式。它基于原型式继承的基础上,通过创建一个仅用于封装继承过程的函数,在函数内部以某种方式来增强对象,最后返回对象。
var Animal = {
name: "动物",
colors: ["black", "white"],
sleep: function () {
console.log(this.name + "正在睡觉!");
},
};
// 封装一个函数
function clone(original) {
// 通过调用函数创建一个新对象
var clone = Object.create(original);
// 以某种方式来增强这个对象
clone.do = function () {
console.log(this.name + "正在做动作!");
};
clone.updateName = function (name) {
this.name = name;
};
// 返回这个对象
return clone;
}
var dog = clone(Animal);
dog.do(); // 动物正在睡觉!
dog.updateName("小狗");
dog.do(); // 小狗正在睡觉!
var cat = clone(Animal);
cat.colors.push("yellow");
console.log(cat.colors); // [ 'black', 'white', 'yellow' ]
console.log(dog.colors); // [ 'black', 'white', 'yellow' ] 共享内存了
寄生组合式继承
这个继承也是ES6
中的extends
关键字,实现 JavaScript
的继承原理,相对于其他的继承是最优的解法
// 父类构造函数
function Animal(name) {
this.name = name;
this.colors = ["black", "white"];
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
// 父类原型方法
Animal.prototype.do = function () {
console.log(this.name + "正在做动作!");
};
// 寄生式继承,用于继承父类原型
function clone(subType, superType) {
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
// 子类构造函数
function Cat(name) {
Animal.call(this, name); // 构造函数继承
}
clone(Cat, Animal);
var Cat1 = new Cat("Cat1");
Cat1.do(); // Cat1正在做动作! 可以访问父类原型上的方法
var Cat2 = new Cat("Cat2");
Cat2.colors.push("yellow");
console.log(Cat1.colors); // [ 'black', 'white' ]
console.log(Cat2.colors); // [ 'black', 'white', 'yellow' ] 内存空间不共享
5. JavaScript事件循环机制?
JavaScript
在设计之初便是单线程,程序运行时,只有一个线程存在,同一时间只能做一件事。
为了解决单线程运行阻塞问题,JavaScript
用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop)
在JavaScript
中,所有的任务都可以分为
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
异步任务:
异步任务分为宏任务(macrotask)与 微任务 (microtask),不同的任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。
微任务对应有:
- next tick queue:process.nextTick
- other queue:Promise的then回调、queueMicrotask
宏任务对应有:
- timer queue:setTimeout、setInterval
- poll queue:IO事件
- check queue:setImmediate
- close queue:close事件
async function async1() {
console.log("1");
await async2();
console.log("2");
}
async function async2() {
console.log("3");
}
console.log("4");
setTimeout(function () {
console.log("5");
new Promise((resolve, reject) => {
console.log("13");
resolve();
console.log("14");
}).then(function () {
console.log("15");
});
}, 0);
setTimeout(function () {
console.log("6");
}, 3);
setImmediate(() => console.log("7"));
process.nextTick(() => console.log("8"));
async1();
new Promise(function (resolve) {
console.log("9");
resolve();
console.log("10");
}).then(function () {
console.log("11");
});
console.log("12");
/**
* 从上往下执行 执行同步任务 打印4
* 567是宏任务压入宏任务队列
* 8是微任务
* 执行async1 打印1 3
* 2在await后面 压入为微队列
* 执行new Promise 打印9 10 .then压入微队列
* 执行打印 12
* 主线程执行完成 打印4 1 3 9 10 12
* 处理微任务队列 打印 8 2 11
* 执行宏任务5 打印 5 13 14 压入微队列.then 15 执行微任务 打印 15
* 执行宏任务6 打印 6
* 执行微任务7 打印 7
* 4 1 3 9 10 12 8 2 12 11 5 13 14 15 6 7
*/