JavaScript 基础
1、基本数据类型:
1.1 基本数据类型:
Number(数值):表示数字,包括整数和浮点数。例如:5、3.14。
String(字符串):表示文本数据,由字符组成。用引号(单引号或双引号)括起来。例如:“Hello”、‘World’。
Boolean(布尔):表示逻辑值,只有两个可能的值:true(真)和false(假)。
Undefined(未定义):表示变量声明但未初始化的值。当访问未初始化的变量时,其值为undefined。
Null(空):表示一个空值或者空对象引用。
Symbol(符号):在ES6中引入的新数据类型,表示唯一且不可变的值。
除了以上的基本数据类型,JavaScript还有一个复杂数据类型:
Object(对象):表示一个键值对的集合,用于存储多个值。可以是原始类型的值或其他对象的引用。
这些基本数据类型在 JavaScript 中都是不可变的(immutable),即它们的值在创建后不能被改变。
1.2、基本类型和引用类型
在JavaScript中,数据类型可以分为基本类型和引用类型。
基本类型(Primitive Types)是指存储简单数据值的类型,它们是不可改变的(immutable)。JavaScript有以下基本类型:
Number(数值):表示数字,包括整数和浮点数。
String(字符串):表示文本数据。
Boolean(布尔):表示逻辑值,true或false。
Undefined(未定义):表示变量声明但未初始化的值。
Null(空):表示一个空值或者空对象引用。
引用类型(Reference Types)是指存储对象的类型,它们是可变的(mutable),并且可以包含多个值。JavaScript中的引用类型包括:
Object(对象):表示键值对的集合,可以存储多个值。
Array(数组):是对象的特殊类型,用于存储有序的数据集合。
Function(函数):是对象的特殊类型,可以执行一些操作。
基本类型的值直接存储在变量中,而引用类型的值则是存储在堆内存中,变量中存储的是对堆内存中的引用。因此,当复制基本类型的值时,会创建一个新的值。而复制引用类型的值时,实际上只是复制了对同一个对象的引用。
总结起来,基本类型在赋值时会复制值本身,而引用类型在赋值时会复制对对象的引用。
1.3.判断一个变量的数据类型
**instanceof:**可以用来检查一个对象是否属于某个特定类或其子类。它是通过检查对象的原型链来确定的,如果对象的原型链中存在指定类或其子类,则返回true,否则返回false。它可以判断引用类型的具体类型,但不能判断基本类型。
**Object.prototype.toString.call():**可以用来获取一个对象的内部属性[[Class]],从而判断其类型。这种方法可以判断基本类型和引用类型,返回的结果是一个字符串,格式为"[object 类型]",其中类型表示对象的具体类型。可以判断基本类型和引用类型的具体类型,但需要通过调用方法来获取类型信息。
typeof():主要用于判断基本类型,如字符串、数字、布尔值等。对于引用类型,除了函数以外,typeof()只能返回"object",无法具体判断其类型
var value = 42;
var type = Object.prototype.toString.call(value);
console.log(type); // 输出 "[object Number]"
2、 事件冒泡和事件捕获
事件冒泡(Event Bubbling)和事件捕获(Event Capturing)是指在处理嵌套元素上的事件时,事件在DOM树中传播的两种不同方式。
事件冒泡是指当一个元素触发了某个事件时,该事件将从最内层的元素开始向父级元素逐级传播,直到达到最外层的祖先元素。这意味着如果你在一个嵌套的元素上注册了事件处理程序,事件将首先触发嵌套元素的处理函数,然后再触发外层元素的处理函数。事件冒泡是默认的事件传播方式。
事件捕获则是相反的过程,事件从最外层的元素开始传播,逐级向内层元素传播,直到触发实际触发事件的元素。对于一个嵌套结构的元素,事件捕获的过程会先触发外层元素的处理函数,然后再触发内层元素的处理函数。
事件冒泡的用途:
方便的事件委托:通过在父元素上监听事件,利用事件冒泡的特性来处理子元素的事件。这样可以减少事件处理程序的数量,提高性能。
动态添加事件:对于动态创建的元素,可以将事件处理程序绑定在它们的父元素上,利用事件冒泡来处理事件。
简化代码结构:通过事件冒泡,可以在父元素上统一处理多个子元素的相似事件,避免重复代码。
事件捕获的用途:
预处理事件:可以在事件传播到目标元素之前拦截和处理事件。适用于需要在事件到达目标元素之前进行一些初始化或预处理操作的场景。
更精细的事件控制:如果需要在事件传播的早期阶段就停止事件传播或阻止默认行为,可以使用事件捕获阶段来实现。
深层次的事件处理:如果需要在嵌套较深的元素结构中进行事件处理,可以利用事件捕获来更精确地控制事件的传播路径。
综合来说,事件冒泡适合用于简化事件处理、提高性能和实现事件委托等场景,而事件捕获则适合在事件传播的早期阶段进行一些特殊处理或控制事件传播路径。
阻止事件冒泡或默认行为
可以使用event.stopPropagation()方法来阻止事件冒泡,使用event.preventDefault()方法来阻止默认行为。
3. 什么是原型链?如何利用原型链实现继承?
原型链是 JavaScript 中用于实现对象之间继承关系的机制。每个对象都有一个指向另一个对象的引用,这个被引用的对象就是该对象的原型(prototype)。而被引用对象的原型又可以有自己的原型,于是就形成了一个链式结构,即原型链。
在 JavaScript 中,每个对象(除了 null)都有一个原型,并且继承了原型对象的属性和方法。当我们试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到相应的属性或方法或者到达原型链的顶端(Object.prototype),如果还是没有找到,则返回 undefined。
利用原型链实现继承的过程可以通过以下步骤来实现:
创建父类(超类)的构造函数,并定义其属性和方法。
创建子类的构造函数,同时将父类的实例作为子类的原型。这样子类就能够继承父类的属性和方法。
可以在子类的构造函数中添加子类特有的属性和方法,也可以重写父类的方法来实现多态。
下面是一个简单的示例来说明如何利用原型链实现继承:
// 定义父类构造函数
function Animal(name) {
this.name = name;
}
// 定义父类的方法
Animal.prototype.sayName = function() {
console.log('My name is ' + this.name);
};
// 定义子类构造函数
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数,继承父类的属性
this.breed = breed;
}
// 将父类的实例作为子类的原型
Dog.prototype = Object.create(Animal.prototype);
// 添加子类的方法
Dog.prototype.bark = function() {
console.log('Woof!');
};
// 创建子类实例
var myDog = new Dog('Buddy', 'Golden Retriever');
myDog.sayName(); // 输出 "My name is Buddy"
myDog.bark(); // 输出 "Woof!"
我们通过原型链的方式实现了子类 Dog 对父类 Animal 的继承。子类 Dog 继承了父类 Animal 的属性和方法,并且还添加了自己的方法。这样就实现了对象之间的继承关系。
图文解析:
-
显示原型
显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。 -
隐式原型
隐式原型是利用__proto__属性查找原型,这个属性指向当前对象的构造函数的原型对象,这个属性是对象类型数据的属性,所以可以在实例对象上面使用:
原型链:
如果某个对象查找属性,自己和原型对象上都没有,那就会继续往原型对象的原型对象上去找,这个例子里就是Object.prototype,这里就是查找的终点站了,在这里找不到,就没有更上一层了(null里面啥也没有),直接返回undefined。
可以看出,整个查找过程都是顺着__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链。所以,原型链也叫作隐式原型链。
正是因为这个原因,我们在创建对象、数组、函数等等数据的时候,都自带一些属性和方法,这些属性和方法是在它们的原型上面保存着,所以它们自创建起就可以直接使用那些属性和方法。
原型和原型链详细深度讲解,好文章
4、遍历对象中的属性
在 JavaScript 中,可以使用不同的方法来遍历一个对象的属性。以下是几种常见的遍历对象属性的方法:
for…in循环:使用for…in循环可以遍历对象的所有可枚举属性,并访问每个属性。
const obj1 = {
name: "qq",
age: "12",
school: "fudan",
};
// for in循环
for (let key in obj1) {
if (obj1.hasOwnProperty(key)) {
// 为了避免遍历到原型链上的属性,通常会在循环体内加上 hasOwnProperty 方法的判断。
console.log(key + ":" + obj1[key]); // name: 11, age: 12, school: fudan
}
}
Object.getOwnPropertyNames():使用Object.getOwnPropertyNames()方法可以获取对象的所有属性名,并返回一个包含属性名的数组。通过遍历这个数组,可以访问对象的属性。
// Object.getOwnPropertyNames() 方法:
Object.getOwnPropertyNames(obj1).forEach((key) => {
console.log(key + ":" + obj1[key]); // name: 11, age: 12, school: fudan
});
console.log( " Object.getOwnPropertyNames(obj1)", Object.getOwnPropertyNames(obj1)); // ['name', 'age', 'school']
Object.keys() 方法:
Object.keys() 方法会返回一个包含对象自身的所有可枚举属性的数组,然后可以使用 forEach 方法遍历这个数组。
// object.kyes()方法
Object.keys(obj1).forEach((key) => {
console.log(key + ":" + obj1[key]); // name: 11, age: 12, school: fudan
});
console.log("Object.keys(obj1)", Object.keys(obj1)); // ['name', 'age', 'school']
Object.entries() ES8+
方法会返回一个包含对象自身的所有可枚举属性键值对的数组,然后可以使用 forEach 方法遍历这个数组。
// Object.entries() 方法(ES8+):
Object.entries(obj1).forEach(([key, value]) => {
console.log(key + ": " + value); // Object.entries() 方法会返回一个包含对象自身的所有可枚举属性键值对的数组,然后可以使用 forEach 方法遍历这个数组。
});
console.log(' Object.entries(obj1)', Object.entries(obj1)) // [["name", "qq"],["age", "12"],["school", "fudan"]]
5、判断一个变量是否为数组
在 JavaScript 中,有几种方法可以判断一个变量是否为数组。以下是常用的几种方法:
Array.isArray() 方法:
使用 Array.isArray() 方法是最可靠和推荐的方法来判断一个变量是否为数组。
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
Array.isArray() 方法会返回 true 如果传入的参数是一个数组,否则返回 false。
instanceof 操作符:
使用 instanceof 操作符也可以判断一个变量是否为数组,但不如 Array.isArray() 方法准确。
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
instanceof 操作符检查原型链,如果变量是指定类型的实例,则返回 true,否则返回 false。
Object.prototype.toString.call() 方法:
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
这种方法通过调用 Object.prototype.toString 方法并传入要检查的变量,然后比较返回的字符串是否为 [object Array] 来判断变量是否为数组。
以上是几种常用的方法来判断一个变量是否为数组。在实际使用中,建议优先使用 Array.isArray() 方法进行判断,因为它更直观和可靠
6、常用数组方法有哪些?
push():向数组末尾添加一个或多个元素,并返回新的长度。
pop():删除并返回数组的最后一个元素。
shift():删除并返回数组的第一个元素,同时将所有其他元素下标减 1。
unshift():向数组的开头添加一个或多个元素,并返回新的长度。
concat():用于合并两个或多个数组,不会修改现有数组,而是返回一个新数组。
slice():从已有的数组中返回选定的元素。
splice():向/从数组中添加/删除项目,然后返回被删除的项目。
indexOf():返回数组中某个元素第一次出现的索引,如果不存在则返回 -1。
lastIndexOf():返回数组中某个元素最后一次出现的索引,如果不存在则返回 -1。
forEach():为数组中的每个元素执行提供的函数。
map():创建一个新数组,其结果是该数组中的每个元素调用一个提供的函数后返回的值。
filter():创建一个新数组,包含通过所提供函数实现的测试的所有元素。
reduce():对数组中的每个元素执行一个提供的函数,将其结果汇总为单个返回值。
find():返回数组中满足提供的测试函数的第一个元素的值。
findIndex():返回数组中满足提供的测试函数的第一个元素的索引。
some():返回数组中是否至少有一个元素通过了所提供函数的测试。
every():返回数组中的所有元素是否都通过了所提供函数的测试。
7、 遍历数组的方法
**for 循环:**使用传统的 for 循环可以遍历数组,通过索引访问数组元素。
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
forEach() 方法:forEach() 方法是数组提供的一种遍历方法,它接受一个回调函数作为参数,在数组的每个元素上执行该回调函数。
const arr = [1, 2, 3];
arr.forEach((element) => {
console.log(element);
});
for…of 循环:for…of 循环是 ES6 引入的新语法,它可以用来遍历可迭代对象,包括数组。
const arr = [1, 2, 3];
for (const element of arr) {
console.log(element);
}
map() 方法:map() 方法可以遍历数组,并将每个元素传递给回调函数进行处理,返回一个新的数组。
const arr = [1, 2, 3];
const newArr = arr.map((element) => {
return element * 2;
});
console.log(newArr);
filter() 方法:filter() 方法可以遍历数组,并根据回调函数的返回值筛选出满足条件的元素,返回一个新的数组。
const arr = [1, 2, 3, 4, 5];
const filteredArr = arr.filter((element) => {
return element % 2 === 0;
});
console.log(filteredArr);
8、ajax请求的几种方法?
XMLHttpRequest对象:XMLHttpRequest是原生的JavaScript对象,它提供了一种在后台与服务器进行数据交换的方法。通过创建一个XMLHttpRequest对象,可以发送异步请求并接收服务器返回的数据。
Fetch API:Fetch API是一种现代的Web API,它提供了一种更简洁和强大的方式来进行网络请求。它使用Promise对象来处理异步操作,并提供了更灵活的请求和响应处理方式。
jQuery.ajax()方法:如果你使用jQuery库,可以使用其提供的ajax()方法来发送异步请求。这个方法封装了XMLHttpRequest对象,并提供了简化的语法和更高级的功能,如自动处理数据格式、跨域请求等。
Axios库:Axios是一个流行的第三方库,用于发送HTTP请求。它提供了简单易用的API,并支持Promise对象和拦截器等功能。Axios可以在浏览器和Node.js环境中使用。
axios请求
Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。
1)首先,需要在项目中安装axios。可以使用npm或者yarn进行安装。
npm install axios
2)在需要发送请求的文件中,引入axios模块。
const axios = require('axios');
// 或者在浏览器中使用 script 标签引入 Axios,无需导入
3)使用axios的方法发送请求,常见的方法有get、post、put、delete等。
// get 请求
axios.get(url)
.then((response) => {
// 请求成功,处理响应数据
console.log(response.data);
})
.catch((error) => {
// 请求失败,处理错误信息
console.error(error);
});
可以通过配置选项来设置请求的URL、请求头、请求体等信息。
使用then方法处理请求成功的响应,使用catch方法处理请求失败的情况。
9、post请求与get请求的区别?
1) 参数传递方式:
GET请求:GET请求的参数会暴露在URL中,可以被缓存、浏览器历史记录等获取到。
POST请求:参数通过请求体传递,不会暴露在URL中,参数通过请求体以键值对的形式传递。
2)参数长度限制:
GET请求:由于参数附加在URL中,URL长度有限制,不同浏览器和服务器对URL长度的限制不同,一般为几千个字符。
POST请求:由于参数通过请求体传递,没有URL长度限制,但是服务器和应用程序可能会对请求体的大小进行限制。
3)安全性:
GET请求:参数暴露在URL中,容易被拦截、修改或泄露,不适合传递敏感信息。
POST请求:参数不暴露在URL中,相对安全,适合传递敏感信息。
4)缓存:
GET请求:由于参数暴露在URL中,可以被浏览器缓存,下次相同的请求可以直接使用缓存结果,提高性能。
POST请求:由于参数不暴露在URL中,不会被浏览器缓存。
5)幂等性:
GET请求:对于相同的请求,多次GET请求不会对服务器产生副作用,即不会改变服务器状态。
POST请求:对于相同的请求,多次POST请求可能会对服务器产生副作用,即可能改变服务器状态。
关键区别总结:
GET 请求用于获取资源,参数附加在 URL 中,可以被缓存,参数较少且不敏感。
POST 请求用于提交数据,参数包含在请求体中,不会被缓存,参数较多或敏感。
10、Promise
在 JavaScript 中,Promise 是一种用于处理异步操作的对象,它代表了一个异步操作的最终完成(或失败)及其结果的承诺。使用 Promise 可以更优雅地处理回调地狱(callback hell)和异步代码。
一个 Promise 可以处于以下三种状态之一:
Pending(进行中):初始状态,既不是成功状态,也不是失败状态。
Fulfilled(已成功):表示操作成功完成。
Rejected(已失败):表示操作失败。
创建一个 Promise 的基本语法如下:
const myPromise = new Promise((resolve, reject) => {
// 异步操作,例如请求数据、定时器等
if (/* 异步操作成功 */) {
resolve("Success"); // 调用 resolve 表示 Promise 成功
} else {
reject("Error"); // 调用 reject 表示 Promise 失败
}
});
Promise 可以通过 then() 方法来处理成功状态和失败状态:
myPromise.then((result) => {
console.log("Promise resolved: " + result); // 处理成功状态
}).catch((error) => {
console.log("Promise rejected: " + error); // 处理失败状态
});
当 Promise 被 resolve(成功)时,会调用 then() 中的回调函数;当 Promise 被 reject(失败)时,会调用 catch() 中的回调函数。
此外,Promise 还支持链式调用,可以通过返回另一个 Promise 实现连续的异步操作:
myPromise.then((result) => {
return doSomethingElse(result);
}).then((newResult) => {
console.log("Chained Promise resolved: " + newResult);
}).catch((error) => {
console.log("Error in promise chain: " + error);
});
通过使用 Promise,可以更清晰地编写和管理异步代码,避免回调地狱,提高代码可读性和可维护性。
Promise难懂?一篇文章让你轻松驾驭
11、async和await用法
async/await 出现的原因
Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。async/await 让代码更少,更简洁
定义:
async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行
async作为一个关键字放在函数前面,表示该函数是一个异步函数,异步函数意味着该函数的执行不会阻塞后面代码的执行;而 await 用于等待一个异步方法执行完成;
await 等待一个 Promise 对象,如果 Promise的状态变成了 resolve 或者 rejcet,那么 async函数会恢复执行。并会阻塞该函数内后面的代码。
使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。
为了优化 .then 链而开发出来的。
详细的介绍和代码分析: 理解异步函数async和await的用法
12、宏任务、微任务?宏任务、微任务有哪些?
首先,我们要先了解下 Js 。js 是一种单线程语言,简单的说就是:只有一条通道,那么在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了 ‘多线程’ ,但是这种“多线程”是通过单线程模仿的,也就是假的。那么就产生了同步任务和异步任务。
宏任务:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage,MessageChannel5. setImmediate,I/O(Node.js)
微任务:
1… Promise
2.process.nextTick(Node.js)
5. Object.observe(已废弃;Proxy 对象替代)
6. MutaionObserver
执行顺序:
执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
这里容易产生一个错误的认识:就是微任务先于宏任务执行。实际上是先执行同步任务,异步任务有宏任务和微任务两种,先将宏任务添加到宏任务队列中,将宏任务里面的微任务添加到微任务队列中。所有同步执行完之后执行异步,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行。之后就一直循环…
案例1:
setTimeout(function(){
console.log('1');
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3');
}).then(function(){
console.log('4')
});
console.log('5');
// 2 5 3 4 1
解析:1.遇到setTimout,异步宏任务,放入宏任务队列中
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(‘5’);输出5;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
6.从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空
案例2:
setTimeout(()=>{
new Promise(resolve =>{
resolve();
}).then(()=>{
console.log('test');
});
console.log(4);
});
new Promise(resolve => {
resolve();
console.log(1)
}).then( () => {
console.log(3);
Promise.resolve().then(() => {
console.log('before timeout');
}).then(() => {
Promise.resolve().then(() => {
console.log('also before timeout')
})
})
})
console.log(2);
解析:
1.遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中;
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(2),输出2;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
6.从微任务队列中取出任务a到主线程中,输出 before timeout;
7.从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
8.从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
9.从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
10.从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空
案例3:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 1 7 6 8 2 4 3 5 9 11 10 12
13、简述 面向对象编程
面向对象编程(Object-Oriented Programming,OOP)是一种程序设计范式,它以对象为中心,将数据和方法封装在对象内部,通过对象之间的交互来实现程序逻辑。面向对象编程强调在程序设计中对现实世界中的对象进行建模,从而使得程序结构更加清晰、易于扩展和维护。
面向对象编程的核心概念包括以下几个方面:
类和对象:
类(Class)是面向对象编程的基本概念,它是一种抽象数据类型的表示,描述了对象的属性和行为。对象(Object)是类的实例,具体化了类中定义的属性和行为。
封装:
封装(Encapsulation)是指将数据和对数据的操作封装在一个对象中,对象对外部提供接口来访问和操作数据,同时隐藏了内部细节。这样可以保护数据不受外部直接访问和修改,提高了安全性和可靠性。
继承:
继承(Inheritance)是一种可以复用已有类的属性和行为的机制,子类可以继承父类的属性和方法,并且可以对其进行扩展或修改。继承可以减少重复代码,提高代码的可维护性和扩展性。
多态:
多态(Polymorphism)是指同一个方法在不同的对象上可以有不同的实现,通过多态可以实现方法的动态绑定,使得程序更灵活、可扩展性更强。
面向对象编程的优点包括代码重用性高、可维护性好、易于扩展和理解等。它能够更好地反映现实世界中的问题,使得程序设计更加直观和自然。因此,面向对象编程已经成为当前主流的程序设计范式之一,在众多编程语言中得到了广泛的应用。
14、面向对象的特点?
JavaScript中的面向对象编程具有以下特点:
**封装:**通过将数据和操作封装在对象中,实现了数据的隐藏和保护。对象的内部状态和行为对外部是不可见的,只能通过对象提供的接口进行访问和操作。
class Circle {
constructor(radius) {
this.radius = radius;
}
// Getter method to access radius
getRadius() {
return this.radius;
}
// Method to calculate area
calculateArea() {
return Math.PI * this.radius ** 2;
}
}
// Create an instance of Circle
let myCircle = new Circle(5);
// Access radius using getter method
console.log("Radius:", myCircle.getRadius());
// Calculate and print area
console.log("Area:", myCircle.calculateArea());
**继承:**通过继承机制,一个对象可以从另一个对象继承属性和方法。继承可以减少代码的重复,提高代码的复用性和可维护性。
class Shape {
constructor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color);
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius ** 2;
}
}
// Create an instance of Circle
let myCircle = new Circle(5, "red");
// Access color using inherited method
console.log("Color:", myCircle.getColor());
// Calculate and print area
console.log("Area:", myCircle.calculateArea());
**多态:**多态性允许一个对象可以以多种不同的方式工作。同一个方法可以根据不同的对象调用产生不同的行为。这种灵活性使得代码更加可扩展和可变。
class Animal {
speak() {
console.log("Animal makes a sound");
}
}
class Dog extends Animal {
speak() {
console.log("Dog barks");
}
}
class Cat extends Animal {
speak() {
console.log("Cat meows");
}
}
// Create instances of Dog and Cat
let dog = new Dog();
let cat = new Cat();
// Call the speak method for Dog and Cat
dog.speak(); // Output: Dog barks
cat.speak(); // Output: Cat meows
原型链:JavaScript中的对象通过原型链来实现继承。每个对象都有一个原型对象,它定义了对象的属性和方法。当访问一个对象的属性或方法时,如果对象本身没有,则会沿着原型链向上查找。
动态性:JavaScript是一门动态语言,允许在运行时动态地添加、修改和删除对象的属性和方法。这种灵活性使得对象的结构和行为可以根据需要进行动态调整。
ES6高频面试题
1、 let, const 和 var
var
var 是在 ES5 中引入的变量声明关键字。
var 声明的变量具有函数作用域,而不是块级作用域。
可以重复声明同一个变量,并且不会报错。
变量提升(hoisting):var 声明的变量会被提升到其作用域的顶部,在变量声明之前就可以访问该变量,但其值为 undefined。
示例:
function example() {
var x = 10;
if (true) {
var x = 20;
console.log(x); // Output: 20
}
console.log(x); // Output: 20
}
let
let 是在 ES6 中引入的块级作用域变量声明关键字。
let 声明的变量具有块级作用域,只在声明的块中有效。
不允许重复声明同一个变量,否则会报错。
不会发生变量提升,必须在声明之后使用变量。
示例:
function example() {
let x = 10;
if (true) {
let x = 20;
console.log(x); // Output: 20
}
console.log(x); // Output: 10
}
const
const 是在 ES6 中引入的常量声明关键字。
const 声明的变量也具有块级作用域,只在声明的块中有效。
声明后的变量值不能再被修改,它是不可变的(但对于对象和数组,其属性或元素可以被修改)。
不允许重复声明同一个变量,否则会报错。
不会发生变量提升,必须在声明之后使用变量。
示例:
function example() {
const x = 10;
if (true) {
const x = 20;
console.log(x); // Output: 20
}
console.log(x); // Output: 10
}
总结:
如果变量需要在整个函数内部访问,可以使用 var。
如果变量仅在特定块级作用域内使用,可以使用 let。
如果需要声明一个不可变的常量,可以使用 const。
2、箭头函数以及箭头函数和普通函数的区别
箭头函数是一种更简洁的函数定义方式,可以减少代码量。
箭头函数没有自己的this值,它会继承外部作用域的this值。
箭头函数不能作为构造函数使用,不能使用new关键字调用。
箭头函数和普通函数的区别:
(1) 箭头函数比普通函数更加简洁
如果没有参数 , 就直接写一个空括号即可 , 如果只有一个参数 , 可以省去参数的括号 如果有多个参数 , 用逗号分割 , 如果函数体的返回值只有一句 , 可以省略大括号。
(2) 箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this, 它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了, 之后不会改变。
(3) 箭头函数继承来的this指向永远不会改变
(4) call()、apply()、bind()等方法不能改变箭头函数中this的指向
(5) 箭头函数不能作为构造函数使用
由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(6) 箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
(7) 箭头函数没有prototype
(8) 补充:箭头函数的this指向哪⾥?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
3、模板字符串
模板字符串是一种允许嵌入表达式的字符串字面量,使用反引号 `` 包裹。
const name = 'Bob';
const greeting = `Hello, ${name}!`;
console.log(greeting); // Output: Hello, Bob!
4、解构赋值
解构赋值是一种便利的表达式,可以将数组或对象的属性值赋给变量。
const { name, age } = { name: 'Alice', age: 30 };
console.log(name, age); // Output: Alice 30
5、下ES6中的类。
类是一种面向对象编程的概念,用于创建对象的模板。
类可以包含属性和方法,可以通过实例化类来创建对象。
6、ES6中的模块化。
模块化是一种将代码分割成独立的模块,每个模块有自己的作用域。
模块可以导出(export)和导入(import)变量、函数、类等。
7、请解释一下ES6中的Promise是什么。
Promise 是用于处理异步操作的对象,可以链式调用 then 方法处理异步操作的结果。
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
8、展开运算符
在对象字面量中,展开运算符可以用来复制对象的属性并添加新的属性。
示例:
const obj1 = { x: 1, y: 2 };
const obj2 = { ...obj1, z: 3 }; // 复制 obj1 的属性并添加新属性 z
console.log(obj2); // Output: { x: 1, y: 2, z: 3 }
Git 相关
1.为什么要使用Git?
Git是一个分布式版本控制系统,用于跟踪文件的变化并协调多人在同一个项目上的工作。使用Git可以实现团队协作、版本管理、代码回滚等功能。
2.Git的工作流程是怎样的?
克隆(Clone):将远程仓库的代码复制到本地。
修改(Modify):在本地进行代码的修改。
提交(Commit):将修改后的代码提交到本地仓库。
推送(Push):将本地仓库的代码推送到远程仓库。
3.Git中的分支(Branch)有什么作用?如何创建和切换分支?
分支是Git中用于并行开发和管理代码的重要概念。通过创建分支,可以在不影响主线代码的情况下进行新功能的开发或问题的修复。
创建分支:git branch
切换分支:git checkout
4.如何解决Git冲突?
执行git pull命令,将远程仓库的最新代码拉取到本地。
手动解决冲突:打开冲突文件,根据提示修改代码,删除或保留需要的部分。
执行git add 命令,将解决冲突后的文件添加到暂存区。
执行git commit命令,提交解决冲突后的代码。
5.如何撤销Git的提交?
撤销最近一次提交并保留修改:git reset HEAD~
撤销最近一次提交并丢弃修改:git reset --hard HEAD~
撤销指定提交:git revert
工作中常用的 git 操作:
提交代码命令:
git pull
git add .
git commit -m ‘feat:上传文件位置’
git push -u origin feature-v2.4
存储:
git stash save: 该命令将临时保存所有修改的文件。
git stash pop: 该命令将恢复最近一次stash(储藏)的文件(应用储藏后并删除储藏)
新建分支:
git branch feature/test 创建一个新分支
git checkout feature/test 切换分支
git push origin feature-v2.5: push到远程
git branch --set-upstream-to=origin/feature-v2.5
新建分支并切换到新分支上:
git checkout -b feature-v2.7
查看分支:
Git branch 查看分支
Git branch -a 查看所有分支,包含远程
删除分支:
如果要删除feature2分支,先切到其他分支上:
git branch -D feature-v2.6.1 // 删除本地
git push origin --delete feature-v2.6.1 // 删除远程分支