js基础
原型
对原型,原型链的理解
原型(Prototype)和原型链是JavaScript中非常重要的概念。
原型(Prototype)是每个函数都有一个prototype属性,这个属性指向函数的原型对象(Prototype Object)。原型对象包含属性和方法,可以被所有实例对象继承。通过构造函数创建的实例对象都有一个__proto__属性,这个属性指向构造函数的原型对象,这样就形成了一条原型链。
原型链的主要作用是实现共享和继承。所有的实例对象都可以继承构造函数的原型对象上的属性和方法,这就是所谓的共享。如果实例对象内部没有某个属性或方法,那么它就会沿着__proto__属性找到构造函数的原型对象,尝试获取这个属性或方法,这就是所谓的原型链查找。
以下是一个简单的示例:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
}
var john = new Person('John');
john.sayHello(); // 输出:Hello, my name is John
在这个例子中,Person是一个构造函数,Person.prototype就是Person的原型对象。我们在这个原型对象上定义了一个sayHello方法,然后通过new Person()创建了一个实例对象john。这个实例对象继承了构造函数的原型对象上的sayHello方法。
因此,当我们调用john.sayHello()时,JavaScript会先在john自身上查找是否有sayHello这个方法,如果没有,那么它会沿着__proto__属性找到构造函数的原型对象(也就是原型链的下一级),尝试获取这个方法。
原型修改,重写
原型修改(Prototype modification)指的是在已有的原型对象上添加新的属性和方法,而不会改变已有实例对象的行为。通过修改构造函数的原型对象,可以影响所有实例对象共享这些属性和方法。
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
}
// 原型修改
Person.prototype.sayHelloTo = function(someone) {
console.log('Hello, ' + this.name + ' is saying hello to ' + someone);
}
var john = new Person('John');
john.sayHelloTo('Alice'); // 输出:Hello, John is saying hello to Alice
以下是一个重写的示例:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
}
function Student(name, grade) {
Person.call(this, name); // 调用父类的构造函数
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype); // 设置子类的原型为父类的实例对象
Student.prototype.constructor = Student; // 修复构造函数引用
// 重写父类的sayHello方法
Student.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name + ', and I am in grade ' + this.grade);
}
var alice = new Student('Alice', 10);
alice.sayHello(); // 输出:Hello, my name is Alice, and I am in grade 10
原型链指向
原型链(Prototype Chain)是指一个对象继承另一个对象的原型链。每个对象都有一个原型对象(Prototype Object),它也拥有自己的原型对象,这样形成了一条原型链。当一个对象被创建时,它的原型对象会被自动设置,并且可以通过__proto__属性进行访问。
在JavaScript中,每个函数都有一个prototype属性,这个属性是一个指针,指向该函数的原型对象。而这个原型对象也有一个constructor属性,指向该函数。因此,原型链在JavaScript中实现了一种继承机制。
在构造函数创建实例对象时,实例对象的__proto__属性指向构造函数的原型对象,而构造函数的原型对象又指向构造函数的父类的原型对象,以此类推,形成了一条原型链。当实例对象调用某个方法或访问某个属性时,如果该对象自身没有这个属性和方法,那么JavaScript会沿着原型链查找,直到找到为止
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
}
function Student(name, grade) {
Person.call(this, name); // 调用父类的构造函数
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype); // 设置子类的原型为父类的实例对象
Student.prototype.constructor = Student; // 修复构造函数引用
var alice = new Student('Alice', 10);
alice.sayHello(); // 输出:Hello, my name is Alice
原型链的终点是什么,如何打印出原型链的终点
原型链的终点是Object.prototype,它是所有对象和函数的原型。在JavaScript中,无法直接访问Object.prototype,但可以通过一些方法间接地打印出原型链的终点。
以下是一种打印原型链终点的方法:
function printPrototypeChainEnd() {
let obj = this;
while (obj.__proto__ !== Object.prototype) {
obj = obj.__proto__;
}
console.log(obj);
}
let person = {
name: "Alice",
sayHello() {
console.log("Hello, my name is " + this.name);
}
};
printPrototypeChainEnd.call(person); // 输出:Function {}
在上面的代码中,printPrototypeChainEnd函数接受一个对象作为参数,并使用while循环遍历该对象的原型链。当对象的__proto__属性等于Object.prototype时,循环结束,并打印出原型链的终点。在示例中,我们通过call方法将person对象作为参数传递给printPrototypeChainEnd函数,并输出结果。
如何获得对象非原型链上的属性
使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:
function iterate(obj){
var res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}
异步编程
异步编程的实现方式
- 回调函数:回调函数是一种异步编程的方式,程序将相关逻辑封装在一个函数中,并把该函数作为参数传给异步函数。当异步函数执行完毕时,会调用该函数来执行相关逻辑。
- Promise对象和Async/Await语法:Promise对象和Async/Await语法是JavaScript中的异步编程特性,提供了一种简单易用的方法来处理异步任务。Promise对象表示一个异步操作的最终完成(或失败)及其结果值。Async/Await语法是基于Promise实现的,提供了更简洁的代码,同时也可以帮助我们更好地组织和管理异步代码。
setTimeout promise async/await的区别
setTimeout、Promise、async/await都是异步编程的方法,它们可以互相配合使用,以实现更复杂的异步逻辑。
setTimeout是JavaScript中的一个内置函数,用于延迟执行代码。它接受两个参数,第一个参数是要执行的函数,第二个参数是延迟的时间(单位为毫秒)。延迟时间到达后,会执行对应的函数。
Promise是JavaScript中的一种对象,用于表示一个异步操作的最终完成(或失败)及其结果值。Promise对象有一个resolve()方法,用于将异步操作的结果值传递给Promise对象的then()方法。Promise对象还有一个reject()方法,用于表示异步操作失败。
async/await是JavaScript中的一种语法,基于Promise实现。async表示函数是异步的,await表示在函数中等待一个Promise对象的结果。使用async/await语法可以让异步代码看起来更像同步代码,提高了代码的可读性和可维护性。
可以将setTimeout和Promise结合使用,实现延迟异步操作。例如:
setTimeout(function() {
Promise.resolve('Hello World!');
}).then(function(value) {
console.log(value);
});
可以将async/await和setTimeout结合使用,实现延迟异步操作。例如:
async function delayHelloWorld() {
await new Promise(resolve => setTimeout(() => resolve('Hello World!'), 1000));
console.log(value);
}
delayHelloWorld();
对promise的理解
Promise是异步编程的一种解决方案,它是一个构造函数,通过它可以生成Promise的实例。Promise的状态有三种:pending(等待)、resolved(成功)、rejected(失败)。Promise对象的状态不受外界影响,只可以通过resolve和reject来改变状态。如果Promise的状态已经变为resolved或者rejected,那么就不能再改变状态了。当Promise的状态变为resolved时,回调函数会立即执行,并传入resolve的值;当Promise的状态变为rejected时,回调函数会立即执行,并传入reject的值。Promise可以用来封装一个异步任务,提供承诺结果,主要用来解决回调地狱的问题,可以有效的减少回调嵌套。
promise的基本用法
- new Promise(function(resolve, reject) {});
- Promise.resolve(value);
- Promise.reject(error);
- promise.then(onFulfilled, onRejected);
- promise.catch(onRejected);
下面简要说明这些用法。
- new Promise:创建一个新的Promise实例,其中resolve和reject是两个函数,分别用于处理异步操作的成功和失败。
- Promise.resolve和Promise.reject:分别用于创建一个已解析的Promise或一个已拒绝的Promise。
- promise.then:用于注册回调函数,当Promise状态变为resolve时,调用onFulfilled回调函数;当Promise状态变为reject时,调用onRejected回调函数。
- promise.catch:用于注册回调函数,当Promise状态变为reject时,调用该回调函数。
promise解决了什么问题
Promise解决了异步操作的问题,具体来说,它解决了回调地狱的问题,使代码更加可读和维护。同时,Promise可以将异步操作队列化,按照期望的顺序执行,并返回符合预期的结果。此外,Promise还可以在对象之间传递和操作,帮助我们处理队列,避免了异步操作函数里的嵌套回调问题。因此,Promise是异步编程的一种非常有用的解决方案。
promise.all和promise.race的区别和使用场景
- Promise.all接受一个Promise的数组作为参数,它将这个数组中的所有Promise实例合并到一个新的Promise实例中。当这个数组中的所有Promise都完成时,新的Promise才会成功,返回一个数组,包含所有成功的结果。如果这个数组中有任何一个Promise失败,新的Promise就会失败,返回第一个失败的Promise的错误。
- Promise.race同样接受一个Promise的数组作为参数,但是它只关心数组中的第一个Promise是成功还是失败。如果有任何一个Promise首先成功或失败,它就会触发回调。因此,它的返回结果和Promise.all不同,它返回的是一个单一的结果,而不是一个数组。
使用场景方面,Promise.all适合于处理多个异步操作,当所有的操作都成功完成时,需要一个包含所有成功结果的数组。例如,在加载多个模块并需要将它们的结果合并到一个新的对象中时,可以使用Promise.all。
Promise.race则适合于竞争条件,即多个异步操作中只关心最先完成的那个结果。例如,在多个定时器或网络请求中,只关心最先返回的那个结果,可以使用Promise.race。
对async/await的理解
当我作为面试者理解async/await时,我认为它们是两个非常重要的关键词,用于处理异步操作,使得代码更加简洁、可读性更好。
async关键词表示函数是异步的,也就是说这个函数不会立即返回结果,而是返回一个Promise对象。在异步函数中,我们可以使用await来等待某个异步操作完成。
await关键词则用于等待异步操作完成,并返回操作的结果。它只能在async函数中使用,并且它会暂停async函数的执行,等待异步操作完成,然后返回操作的结果。
使用async/await可以避免回调地狱,使代码更加简洁易懂。同时,它们也提供了一种更好的方式来处理错误和异常,例如在异步操作中捕获错误并处理它们。
总的来说,async/await是JavaScript中非常重要的特性,它们可以使我们的代码更加可读、可维护,并且更容易处理异步操作。
await到底在等啥
await等待的是一个表达式,这个表达式的计算结果是Promise对象或者其它值(换句话说,就是没有特殊限定)。
因为async函数返回一个Promise对象,所以await可以用于等待一个async函数的返回值——这也可以说是await在等async函数,但要清楚,它等的实际是一个返回值。注意到await不仅仅用于等Promise对象,它可以等任意表达式的结果,所以,await后面实际是可以接普通函数调用或者直接量的
async/await对比promise优势
- 代码简洁性:使用async/await可以让代码更加简洁,不需要像使用Promise那样编写大量的.then和匿名函数处理Promise的resolve值,也不需要定义多余的data变量,同时避免了嵌套代码。
- 错误处理:使用async/await可以让try/catch语句同时处理同步和异步错误,而在Promise中,错误发生在Promise中时,try/catch无法处理JSON.parse失败的情况。
综上所述,async/await在代码简洁性和错误处理方面相比Promise具有优势。
async/await如何捕获异常
在async/await中,可以通过try/catch语句来捕获异常。如果在async函数中有任何未捕获的错误,整个async函数将返回一个被拒绝的Promise,因此,在async函数中,应该始终使用try/catch来捕获并处理任何可能出现的异常。
以下是一个使用async/await捕获异常的示例代码:
async function example() {
try {
// 异步操作
const result = await someAsyncOperation();
// 其他操作
} catch (error) {
// 捕获并处理异常
console.error(error);
}
}
example();
并发与并行的区别
并发(Concurrency)是指在一个时间段内发生多个事件,这些事件可能会相互影响。并发通常是在一个处理器上运行多个任务,这些任务可能会交替地执行。在并发的情况下,一个时间段内可能同时存在多个进程或线程。
并行(Parallelism)是指在同一时刻发生多个事件,这些事件是相互独立的,不会相互影响。并行通常是在多个处理器上同时运行多个任务,这些任务可以同时执行。在并行的情况下,同一时刻内可能存在多个进程或线程。
因此,并发和并行的主要区别在于它们发生的时间和方式。并发是在同一处理器上运行多个任务,这些任务交替执行,而并行是在多个处理器上同时运行多个任务,这些任务可以同时执行。
什么是回调函数,回调函数有什么缺点,如何解决回调地狱的问题
回调函数是一种通过指针调用的函数,它不是由函数的实现方直接调用的,而是在特定的时间或者条件,由另一方函数调用。回调函数的优点是可以把调用者和被调用者区分开,简化代码,提高程序的灵活性。
回调函数的缺点主要有两个:一是嵌套函数存在严重的耦合,牵一发而动全身,代码可复用性不强,可阅读性差;二是错误处理比较艰难,不能使用try catch 和不能直接return。
解决回调地狱的问题主要有以下几种方法:
- 使用Promise:将异步操作封装成Promise,通过then和catch方法处理异步操作的结果,避免回调地狱的问题。
- 使用async/await:将异步操作封装成async函数,通过await关键字等待异步操作的结果,避免回调地狱的问题。
- 保持代码浅模块化:将代码拆解为多个小函数,每个函数只负责单一的任务,减少函数之间的耦合度。
- 使用事件驱动编程:将程序拆解成多个独立的事件,每个事件对应一个处理函数,通过事件驱动程序运行,避免回调地狱的问题。
setTimeout setInterval requestAnimationFrame各有什么特点
-
setTimeout:
- 定时器函数在设定时间后执行,如果任务队列中有其它事件,那么要等待前面的任务执行完成后再执行。
- 时间间隔参数只是指定了把动画代码添加到浏览器任务队列中等待执行的时间,而不是立即执行函数。
- 定时器是异步的,函数会立即返回,不等待定时器。
-
setInterval:
- 每隔一段时间执行一次函数,如果任务队列中有其它事件,那么要等待前面的任务执行完成后再执行。
- 用于循环执行代码,每经过一定的时间间隔就执行一次指定的函数。
- 也会立即返回,不等待定时器。
-
requestAnimationFrame:
- 与屏幕的刷新频率保持一致,把每一帧中的DOM操作几种起来,在一次重绘或回流中完成。
- 页面最小化之后,requestAnimationFrame会停止执行,页面打开之后会继续上次运行的位置继续运行,有效节省CPU开销。
- 在隐藏或不可见元素中将不进行回流或重绘,更少地使用CPU、GPU和内存。
综上所述,setTimeout和setInterval适用于简单的定时任务,而requestAnimationFrame则更适合于动画和复杂的定时任务,能够更好地配合浏览器的刷新频率,以达到流畅的动画效果。
执行上下文作用域,闭包
对闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() { let a = 1 window.B = function () { console.log(a) } } A() B() // 1
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
- 第二种就是使用
setTimeout的第三个参数,这个参数会被当成timer函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
- 第三种就是使用
let定义i了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
对作用域,作用域链的理解
作用域
- 全局作用域:在所有函数之外声明的变量具有全局作用域。这些变量在程序的整个生命周期中都是可访问的。
- 局部作用域:在函数内部声明的变量具有局部作用域。这些变量只能在定义它们的函数内部访问。
- 块级作用域:在代码块(如循环、条件语句、 switch等)中声明的变量具有块级作用域。这些变量只能在定义它们的代码块内部访问。
作用域链
作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
对执行上下文的理解
- 执行上下文是JavaScript代码执行时的环境,它定义了变量和函数的可访问性以及执行规则。
- JavaScript中的执行上下文可以分为三种类型:全局执行上下文、函数执行上下文和Eval执行上下文。
- 全局执行上下文是最外层的JavaScript环境,当代码执行时,全局执行上下文被创建并开始执行。在这个环境中,全局变量和函数都是可访问的。
- 函数执行上下文是函数被调用时创建的环境。在这个环境中,函数内部的变量和函数是可访问的,但无法直接访问全局变量和函数。如果函数内部调用了另一个函数,那么会创建一个新的函数执行上下文,并将其附加到当前执行上下文的原型链上。
- Eval执行上下文是使用Eval函数创建的环境。由于Eval函数的强大能力,Eval执行上下文具有与全局执行上下文相同的访问权限。然而,由于Eval函数的性能问题,建议尽量避免使用它。
- 每个执行上下文都有一个与之关联的作用域链,用于确定变量和函数的可访问性。作用域链的前端始终是当前执行上下文的变量对象,而链的后端则指向全局对象。在作用域链中,函数可以访问其外部函数的变量,但如果外部作用域没有定义变量,则访问会失败。
总之,理解执行上下文和作用域链对于编写高质量的JavaScript代码非常重要,它可以帮助我们避免变量污染、提高代码的可读性和维护性。
this call apply bind
在 JavaScript 中,this 是一个非常重要的概念,它表示函数运行时的上下文。this 的值取决于函数的调用方式。
call、apply 和 bind 是 JavaScript 中的三个函数方法,它们都可以用来改变函数的 this 上下文。
call方法:func.call(thisArg, arg1, arg2, ...)- 调用一个函数,设置其
this为thisArg,并接受额外的参数作为函数的参数。 thisArg可以是任何值,包括null或undefined。- 如果省略
thisArg,则this的值默认为全局对象(在浏览器中是window)。 - 返回值是调用的函数返回的结果。
示例:
function greet(name) {
console.log('Hello, ' + name);
}
var person = {
name: 'Alice'
};
greet.call(person, 'John'); // 输出:Hello, John
在上面的例子中,greet 函数的 this 被设置为 person 对象,并接受额外的参数 'John'。
2. apply 方法:
* `func.apply(thisArg, [argsArray])`
* 调用一个函数,设置其 `this` 为 `thisArg`,并接受一个包含参数的数组或类数组对象作为函数的参数。
* `thisArg` 可以是任何值,包括 `null` 或 `undefined`。
* 如果省略 `thisArg`,则 `this` 的值默认为全局对象(在浏览器中是 `window`)。
* 返回值是调用的函数返回的结果。
示例:
function greet(name) {
console.log('Hello, ' + name);
}
var person = {
name: 'Alice'
};
greet.apply(person, ['John']); // 输出:Hello, John
在上面的例子中,greet 函数的 this 被设置为 person 对象,并接受一个包含参数的数组 ['John']。
3. bind 方法:
* `func.bind(thisArg, arg1, arg2, ...)`
* 创建一个新的函数,设置其 `this` 为 `thisArg`,并接受额外的参数作为函数的参数。这个新函数可以在之后被调用,而不需要通过原始函数来调用。
* `thisArg` 可以是任何值,包括 `null` 或 `undefined`。
* 如果省略 `thisArg`,则 `this` 的值默认为全局对象(在浏览器中是 `window`)。
* 返回值是一个新的函数。
示例:
function greet(name) {
console.log('Hello, ' + name);
}
var person = {
name: 'Alice'
};
var boundGreet = greet.bind(person, 'John'); // 创建一个新的函数 boundGreet,其 this 为 person 对象,并接受额外的参数 'John'。
boundGreet(); // 输出:Hello, John
在上面的例子中,我们使用 bind 方法创建了一个新的函数 boundGreet,它的 this 被设置为 person 对象,并接受额外的参数 'John'。然后我们可以直接调用这个新函数,而不需要通过原始的 greet 函数来调用。
对this对象的理解
call()和apply()的区别
call()和apply()在JavaScript中都用于改变函数的执行上下文,也就是修改函数内部this的指向。它们的主要区别在于传参的方式和执行的性能。
- 传参方式:call()传入的参数需要按顺序排列,第一个参数是this,然后是其他参数。而apply()传入的参数可以是一个数组,第一个参数是this,第二个参数是数组。这意味着如果你需要传递多个参数,可以将它们组合为一个数组来使用apply()。
- 执行性能:由于call()需要按顺序排列参数,所以在传递多个参数时,会有一些性能开销。而apply()则直接将整个数组作为参数传入,因此在传递多个参数时,性能更优。
总结来说,call()和apply()都可以改变函数的执行上下文,但在传参方式和执行性能上有所不同。根据具体的使用场景和需求,可以选择更适合的方法
实现call,apply,bind函数
面向对象
对象创建的方式有哪些
- 使用字面量创建对象
这是创建对象最简单的方法。你只需定义一个变量,然后给它分配一个对象字面量。
let obj = {};
你也可以在对象字面量中直接定义属性和方法:
let obj = {
name: "John",
age: 30,
sayHello: function() {
console.log("Hello, my name is " + this.name);
}
};
- 使用构造函数创建对象
你可以创建一个构造函数,然后使用new关键字来创建对象。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("Hello, my name is " + this.name);
};
}
let obj = new Person("John", 30);
- 使用Object.create()创建对象
Object.create()方法创建一个新对象,使用现有的对象作为新创建对象的__proto__。
let personProto = {
sayHello: function() {
console.log("Hello, my name is " + this.name);
}
};
let obj = Object.create(personProto);
obj.name = "John";
obj.age = 30;
- 使用类创建对象 (ES6及以后的版本)
类是构造函数的语法糖。你可以使用类来创建对象,类可以继承另一个类。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello, my name is " + this.name);
}
}
let obj = new Person("John", 30);
- 使用Object.freeze()创建冻结对象 (ES5及以后的版本)
Object.freeze()方法将一个对象冻结,使得任何修改该对象的尝试都会被忽略。冻结的对象是只读的。如果尝试修改一个冻结的对象,那么该操作会静默失败,不会抛出错误。这可以用来创建一个不可变的对象。
注意:Object.freeze()方法创建的不是一个新的对象,而是冻结现有的对象。
对象继承的方式有哪些
- 原型链继承
这是 JavaScript 中最原始的继承方式。通过原型链,一个对象可以继承另一个对象的属性和方法。
优点:
- 实现真正意义上的继承,子类拥有和父类完全相同的属性和方法。
- 子类可以重写父类的方法,并且不会影响父类的方法。
缺点:
- 所有实例都会继承原型上的属性,可能会导致一些预期之外的副作用。
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
var child = new Child();
child.sayName(); // 'parent'
- 构造函数继承
这种方式是通过在子类的构造函数中调用父类构造函数来实现继承。
优点:
- 简单易懂,易于实现。
- 子类可以继承父类的属性和方法。
缺点:
- 子类实例并不是父类的实例,只是子类的实例。
- 无法继承父类的原型属性和方法。
- 每个子类都有父类实例函数的副本,影响性能。
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'child';
}
var child = new Child();
console.log(child.name); // 'parent'
- 组合继承(也称为伪经典继承)
这种方式结合了原型链继承和构造函数继承。首先使用构造函数继承,然后通过原型链进行进一步的继承。
优点:
- 结合了原型链和构造函数的优势,子类既可以继承父类的属性和方法,也可以重写父类的方法。
- 可以实现函数复用,减少了内存消耗。
缺点:
- 代码较为复杂,需要同时处理构造函数和原型链的继承。
- 仍然无法完全解决原型属性和方法的问题。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent(); // 使用构造函数创建原型对象
Child.prototype.constructor = Child; // 重设构造函数
var child = new Child();
child.sayName(); // 'parent'
- 原型式继承
这种方式是通过创建一个新的对象,然后将这个新对象的原型设置为父类的一个实例。
优点:
- 实现简单,只需将子类的原型指向父类的实例即可。
- 子类可以重写父类的方法,并且不会影响父类的方法。
缺点:
- 所有实例都会继承原型上的属性,可能会导致一些预期之外的副作用。
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。
总的来说,原型式继承简单易懂,但可能存在一些预期之外的副作用和性能问题。
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
var child = new Child();
console.log(child.name); // 'parent'
- 寄生组合继承
这种方式结合了构造函数和原型链的优点,它通过调用父类的构造函数,并将其结果用作子类的原型,然后修改子类的原型为另一个对象,该对象的原型是子类自身。
优点:如果写es5推荐使用
- 高效,因为它只调用了一次父构造函数,并且避免了在父原型上创建不必要的、多余的属性。
- 保持了原型链的完整性,因此可以正常使用 instanceof 和 isPrototypeOf。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent(); // 使用构造函数创建原型对象
Child.prototype.constructor = Child; // 重设构造函数
Child.prototype.sayChildName = function() {
console.log('Child');
};
var child = new Child();
child.sayName(); // 'parent'
- 类继承 (ES6 引入)
ES6 引入了类(class)语法,这是一种基于类的继承方式。使用 extends 关键字来继承父类。
优点:
- 语法简洁清晰,易于理解和使用。
- 支持继承自任何内置的 JavaScript 对象和类。
缺点:
- 在某些情况下,可能会破坏封装性,因为构造函数中的属性可能会暴露给外部代码。
- 对于旧版浏览器可能存在兼容性问题。
class Parent {
constructor() {
this.name = 'parent';
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor() {
super(); // 调用父类的构造函数
this.type = 'child';
}
}
let child = new Child();
child.sayName(); // 'parent'
es6
let const var的区别
"let, const, var 是编程语言中用来声明变量的关键字,但它们在本质上有一些区别。
var:这是最常见的变量声明关键字。当你使用 'var' 关键字声明一个变量时,该变量会被分配一个内存空间,并且可以在其作用域内被多次赋值。这意味着你可以在程序的不同部分更改变量的值。
例如,在 JavaScript 中:
var x = 10;
console.log(x); // 输出:10
x = 20;
console.log(x); // 输出:20
let:使用 'let' 关键字声明的变量拥有块级作用域。这意味着变量只在其声明的代码块内有效。在代码块之外,你不能访问或修改这个变量。此外,'let' 声明的变量在声明之前是不可用的,这种现象被称为“暂时性死区”。
例如,在 JavaScript 中:
if (true) {
let x = 10;
console.log(x); // 输出:10
}
console.log(x); // 抛出错误,因为 x 在这个作用域内是不可见的
const:'const' 是另一种声明变量的关键字。与 'var' 和 'let' 不同,'const' 声明的变量是只读的。这意味着一旦一个常量被赋值,就不能再改变其值。常量的作用域和 'let' 一样是块级作用域。
例如,在 JavaScript 中:
const x = 10;
console.log(x); // 输出:10
x = 20; // 抛出错误,因为 x 是只读的
总结来说,'var' 可以多次赋值,'let' 和 'const' 是块级作用域,其中 'const' 是只读的。"注意const如果赋值是对象整个引用不能修改,但是可以修改某个属性值
const 对象的属性可以修改吗
可以
如果new一个箭头函数的会怎么样
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
new操作符的实现步骤如下:
- 创建一个对象
- 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
- 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
- 返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
箭头函数与普通函数的区别
- 外形:箭头函数使用箭头
=>定义,而普通函数则没有。 - 匿名性:箭头函数都是匿名函数,而普通函数可以是匿名函数,也可以是有具名函数。
- 构造函数:箭头函数不能用于构造函数,而普通函数可以用于构造函数,以此创建对象实例。
- this的指向:在箭头函数中,this的指向不同于普通函数。在普通函数中,this总是指向调用它的对象,如果用作构造函数,它指向创建的对象实例。
- arguments对象:每一个普通函数调用后都具有一个arguments对象,用来存储实际传递的参数。但是箭头函数并没有此对象。
- 其他区别:箭头函数不能作为Generator函数,没有prototype原型对象,没有super,也没有new.target。
- 执行上下文:普通函数有自己的执行上下文(context),而箭头函数没有。这意味着箭头函数无法访问到上下文中的变量和方法,而普通函数可以。
- 函数参数:箭头函数没有arguments对象,因此无法通过arguments来获取传递的所有参数。而在普通函数中,arguments可以获取所有传递的参数。
- 变量提升:箭头函数没有变量提升(Hoisting)现象。这意味着在箭头函数中,变量必须在函数的开始处声明,否则会引发ReferenceError。而在普通函数中,变量可以声明在函数的任何位置。
- 异步操作:箭头函数不能用于异步操作,因为它们不是Promise。而普通函数可以通过返回Promise来进行异步操作。
- 对象方法:箭头函数不能直接调用对象的方法,因为它们没有绑定对象。而普通函数可以调用对象的方法,只要将对象作为参数传递给函数即可。
- 模块导出:在模块导出时,箭头函数不能作为默认导出,而普通函数可以。
总之,箭头函数和普通函数在许多方面都存在差异,包括外形、匿名性、构造函数、this的指向、arguments对象、执行上下文、函数参数、变量提升、异步操作、对象方法和模块导出等方面。根据具体的使用场景和需求,可以选择最适合的函数类型。
箭头函数的this指向哪里
箭头函数没有自己的this上下文,而是继承了包含它的函数的this。这意味着,当箭头函数在非函数上下文中使用时,它的this指向全局对象(非严格模式下)或undefined(严格模式下)。
以下是一些例子来更好地说明箭头函数的this指向:
- 在普通函数中,
this指向调用它的对象:
function myFunction() {
console.log(this);
}
var obj = {
myFunction: myFunction
};
obj.myFunction(); // 输出:obj
- 在箭头函数中,
this指向包含它的函数:
function myFunction() {
var arrowFunction = () => {
console.log(this);
};
arrowFunction(); // 输出:全局对象(非严格模式下)或undefined(严格模式下)
}
var obj = {
myFunction: myFunction
};
obj.myFunction(); // 输出:obj,因为箭头函数没有自己的this上下文,而是继承了myFunction的this
请注意,箭头函数没有自己的prototype属性,这意味着它们不能用作构造函数,不能直接实例化。如果尝试实例化一个箭头函数,会抛出TypeError。
扩展运算符的作用及使用场景
扩展运算符(Spread Operator)是 JavaScript ES6 引入的一个新特性,它的主要作用是扩展数组或对象。
扩展运算符的作用
- 扩展数组:将一个数组序列展开为参数序列。
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5, 6]; // [1, 2, 3, 4, 5, 6]
- 合并数组:在解构赋值中,可以用于合并数组。
let [...part1, ...part2] = [1, 2, 3];
// part1 = [1, 2]; part2 = [3];
- 展开对象:将一个对象所有可枚举的自身属性值(不包括继承的)展平,即为展开操作。
let obj = { a: 1, b: 2 };
let { ...newObj } = obj;
console.log(newObj); // { a: 1, b: 2 }
扩展运算符的使用场景
- 函数参数展开:可以将一个数组或者类数组对象(比如arguments对象)展开,然后作为函数的参数。
- 对象属性合并:在对象解构赋值中,可以使用扩展运算符来合并多个对象。
- 数组合并:使用扩展运算符可以合并两个数组。
- 函数返回值收集:使用扩展运算符可以将所有函数返回的元素收集到一个新的数组中。
- 调用函数:可以将一个函数调用展开到一个参数列表中。
proxy可以实现什么功能
- 对象的拦截和控制:可以对对象的属性访问、赋值、函数调用等操作进行拦截和控制,从而实现对对象行为的定制。
- 数据劫持:可以通过Proxy实现数据双向绑定、深度监听、表单校验等数据劫持操作。
- 权限控制:可以使用Proxy实现对象属性的访问权限控制,限制一些敏感属性的访问。
- 性能优化:可以使用Proxy进行缓存、懒加载和单例模式等性能优化操作。
- 改善网络性能:Proxy可以缓存网页、图像、视频等内容,当用户再次访问该内容时,可以从缓存中读取,避免重复下载和重复请求,提高访问速度。同时,Proxy还可以压缩页面内容,减少传输数据量,提高用户访问速度。
- 实现安全访问控制:Proxy可以限制用户访问某些不安全或敏感内容,保护重要的企业数据,防止数据泄露。
- 实现匿名性:使用代理服务器访问互联网时,Proxy会为客户端提供一个虚拟IP地址,从而实现匿名访问。此时,目标服务器只能看到Proxy的IP地址,无法得知客户端的真实IP地址和身份信息。这种匿名访问可以有效保护网络隐私和安全,防止网络攻击和追踪。
综上所述,Proxy具有较为丰富的功能,可以在不同场景下提供便利。
数据类型
javascript有哪些数据类型,它们的区别
number string boolean undefined null symbol bibint object
symbol bigint是es6新增的 symbol 代表创建后独一无二不可变的数据类型,它主要为了解决可能出现的变量冲突的问题 1
bigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用bigint可以安全地存储和操作大整数,即使这个数已经超出了number能够表示的安全整数范围
数据类型分为原始数据类型和引用数据类型
原始数据类型直接存储在栈中,固定大小,占据空间小
引用数据类型存储在堆中,大小不固定,如果存储在栈中,将会影响程序运行的性能,引用数据类型在栈中存储了指针,该指针向堆中该实体的起始地址,当解释器寻找引用值时,会首先检索其栈中的地址,取得地址后从堆中获得实体
数据类型检测的方式有哪些
typeof instanceof constructor Object.prototype.toString.call()
typeof
typeof 2 //number
typeof '123' //string
typeof function //function
typeof [1,2] //object
typeof {} //object
typeof null //object
typeof undefined //undefined
typeof true //boolean
instanceof 可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型
console.log(2 instanceof Number) //false
console.log(true instanceof Boolean) //false
console.log('str' instanceof String) //false
console.log([] instanceof Array) //true
console.log(function(){} instanceof Function) //true
console.log({} instanceof Object) //true
constructor ??
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
判断数组的方式
Object.prototype.toString.call()
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
console.log(Object.prototype.toString.call([1,2])) //'[object Array]'
console.log(Object.prototype.toString.call({})) //'[object Object]'
console.log(Object.prototype.toString.call('123')) //'[object String]'
Array.isArray(arr)
obj instanceof Array
Array.prototype.isPrototypeOf(arr)
同样检测对象obj调用toString方法,obj.toString的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么
这是因为toString是Object的原型方法,而Array,function等类型作为Object的实例,都重写了toString方法,不同的对象类型调用toString方法时,根据原型的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
null和undefined区别
首先都是基本数据类型,这两个数据型分别都只有一个值
undefined含义是未定义,null代表含义是空对象,一般变量声明了但是还没有定义的时候会返回undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化
undefined在js中不是一个保留字,为什么有时用viod 0来代表undefied作判断更为准确
当对这两种类型作用typeof进行判断时,null类型化会返回Object,这是一个历史遗留的问题,当使用双等号对两种进行比较是true,使用三等号时会是false
typeof null的结果是什么,为什么
Object 这和js存储位有关系这里就不记了,比这个更重要的需要记忆
intanceof操作符的实现原理及实现
instanceof 运算符用于判断构造函数的prototype属性是否出现在对象的原型链中的任何位置
function myInstanceof(left,right){
//获取对象的原型
let proto=Object.getPrototypeOf(left)
//获取构造函数的prototype对象
let prototype=right.prototype
//判断构造函数的prototype对象是否在对象的原型链上
while(true){
if(!proto) return false
if(proto===prototype) return true
//如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法来获取指定对象的原型
proto=Object.getPrototypeOf(proto)
}
}
为什么0.1+0.2!==0.3 如何让其相待
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这样让其相等
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
原因 有点复杂有点深,自行搜索我是忽略了不做为记忆
如何获取安全的undfined值
void 0 ,在js中undefined可以定义为变量做修改
typeof NaN的结果是什么
NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
typeof和instanceof区别
typeof是判断js中基本数据类型的 instanceof 判断对象类型
isNaN和Number.isNaN函数的区别
函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回true,因此非数字值传入也会返回true,会影响NaN的判断
函数Number.isNaN会首先判断传入参数是否为数字,如果是数字再继续判断是否为NAN,不会进行数据类型的转换,这种方法对于NAN的判断更为准确
==操作符的强制类型转换规
其他值到字符串的转换规则
- null和undefined类型,null转null undefined转undefined
- boolean类型,true转转换‘true’,false 转‘false’
- number类型的值直接转换,不过那些极小和极大的数字会使用指数形式
- Symbol类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误
- 对变通对象来说,除非自定义toString()方法,否则会调用toString (Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
其他值到数字值的转换规则
- undefined类型转换为NaN
- null类型转换0
- boolean类型值转换true-1 false-0
- string类型的值不能转为数字会报错
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
其他值到布尔类型的值的转换规则
- 假值 undefined null false +0 -0 NaN ‘’
- 假值的布尔值强制类型转换结果是false
||和&&操作符的返回值
*对于||来说,如果条件判断结果为true就返回第一个操作数据的值,如果为false就返回第二个操作数的值 *&&则相反,如果条件判断结果为true就返回第二个操作数的值,如果为false就返回第一个操作数的值
Object.is()与比较操作符“===”,“==”的区别
- 使用双等号==,进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较
- 使用三等号===进行相等判断时,如果两边的类型不一致时,不会做强制类型转换,直接返回false
- Object.is(0,-0) //false Object.is(NaN,NaN) //true 一般情况下是处理了一些特殊的情况
什么是javascript中的包装类型
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。
JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:
var a = 'abc'
Object(a) // String {"abc"}
也可以使用valueOf方法将包装类型倒转成基本类型:
var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
看看如下代码会打印出什么:
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // never runs
}
答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。
javascript中如何进行隐式类型转换
+操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
-、*、``操作符
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
- 对于
==操作符 操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
- 对于
<和>比较符 如果两边都是字符串,则比较字母表顺序:
'ca' < 'bd' // false
'a' < 'b' // true
其他情况下,转换为数字再比较:
'12' < 13 // true
false > -1 // true
+操作符什么时候用于字符串的拼接
- 简单来说就是如果+的其中一个操作数是字符串,则执行字符串拼接,否则执行数字加法
- 于对除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
为什么会有bigint的提案
javsscript中的number.MAX_SAFE_INTEGER表示最大安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(小数除外)但是一旦超过这个范围,js就会出现计算不准确的情况,这在大数计算的时候不得不依靠一些第三方库进行解决,因此官方提出了Bigint来解决此问题
object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
- 都是浅拷贝
- Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象,然后把所有的源对象合并到目标对象中,它会修改了一个对象,因些会触发es6 setter
- 扩展操作符使用它时,数组或者对象中的每一个值都会被拷贝到一个新的数组或者对象中,它不复制继承的属性或类的属性,但是它会复制Es6的symbols属性
扩展运算符
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
Object.assign()
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
javascript基础
什么是事件监听
首先需要区别清楚事件监听和事件监听器。
在绑定事件的时候,我们需要对应的书写一个事件处理程序,来应对事件发生时的具体行为。
这个事件处理程序我们也称之为事件监听器。
当事件绑定好后,程序就会对事件进行监听,当用户触发事件时,就会执行对应的事件处理程序。
关于事件监听,W3C 规范中定义了 3 个事件阶段,依次是捕获阶段、目标阶段、冒泡阶段。
- 捕获阶段:在事件对象到达事件目标之前,事件对象必须从 window 经过目标的祖先节点传播到事件目标。 这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件。
- 目标 阶段:事件对象到达其事件目标。 这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播。
- 冒泡 阶段:事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到 window。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理。
new操作符的实现原理
- 首先创建一个新的空对象
- 设置原型,将对象的原型设置为函数的prototype对象
- 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
- 判断函数的返回值类型,如果是值类型,返回创建的对象,如果是引用类型,返回这个引用类型的对象
function newInstance(constructor, ...args) {
// 创建一个空对象,且 __proto__ 指向 constructor.prototype
const obj = Object.create(constructor.prototype);
// 执行构造函数并绑定 this 到新对象上
const result = constructor.apply(obj, args);
// 如果构造函数返回了一个对象,则返回该对象
if (result && (typeof result === "object" || typeof result === "function")) {
return result;
}
// 否则返回新对象
return obj;
}
// 示例使用
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}.`);
};
const person1 = newInstance(Person, "Allen");
person1.greet(); // 输出 "Hello, I'm Allen."
map和object的区别
map和weakMap的区别
javascrip有哪些内置的对象
全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类:
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等
(10)控制抽象对象 例如 Promise、Generator 等
(11)反射。例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他。例如 arguments
总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
常用的正则表达式有哪些
zhuanlan.zhihu.com/p/458177770 zhuanlan.zhihu.com/p/469973110
// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;
// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;
// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
对json的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
- JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
- JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
json格式字符串和json对象的区别和应用场景?
- json格式字符串是为了方便通信,即前端和后台代码,通信的数据格式一般情况都是字符串。有的情况是需要json格式的字符串更方便,主要是为了把json格式字符串转换为json对象,所以就有了json格式的字符串(方便通信)
- json对象目的是为了方便获取对象的字段,不管是js json对象,还是java里的json对象(方便读数据)
应用场景: JSON字符串适用于数据的传输和存储,而JSON对象适用于直接在JavaScript中进行数据处理和操作。在实际开发中,需要根据具体的需求和场景选择使用JSON字符串还是JSON对象。
javascript脚本延迟加载的方式有哪些
延迟加载就是等页面加载完成之后再加载javascript文件,js延迟加载有助于提高页面加载速度
- defer属性:给js脚本添加defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞,多个设置了defer属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样
- async属性,给js脚本添加asyncn属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完后立即执行js脚本,这个时候如果文档没有解析完成的话同样会阻塞,多个async属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行
- 动态创建DOM方式,动态创建DOM标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script标签来引入js脚本
- 使用setTimeout延迟方法,设置一个定时器来延迟加载js脚本文件
- 让js最后加载,将js脚本放在文档的底部,来使js脚本尽可能在最后来加载执行
defer 与 async 的区别
defer属性:
- 定义:defer属性用于指定脚本在文档完全解析完成后才执行。
- 应用场景:适合用于需要在整个页面解析完成后才执行的功能,以避免因为脚本加载导致页面内容暂时中断或错误。例如,在脚本中需要使用页面中定义的变量或对象,或者对DOM元素进行操作时,使用defer属性可以确保脚本在页面完全解析后才执行。
- 特点:
- 不一定会在DOMContentLoaded事件前执行,但一定会在load事件前执行。
- 在HTML解析完成后会被执行,即它会在js的模块执行完之前执行。
- defer属性的脚本会按照在文档中出现的顺序执行,因此脚本的加载和执行都是同步的,即先加载第一个,然后执行第一个,再加载第二个,然后执行第二个,以此类推。
async属性:
- 定义:async属性用于指定脚本异步加载和执行。
- 应用场景:适合用于不依赖于页面其他元素的脚本,可以与其他脚本并行加载和执行。例如,用于加载第三方库或缓存的脚本可以使用async属性,以避免阻塞页面的加载和渲染。
- 特点:
- 异步执行:脚本会在后台异步加载,不会阻塞页面的解析。
- 没有规定脚本加载的顺序:多个异步脚本可能同时加载,也可能按任意顺序执行。
- 不能保证在DOMContentLoaded事件前执行,但一定会在load事件前执行。
- async属性的脚本会按照在文档中出现的顺序执行,但加载和执行是异步的,即不会等待上一个脚本执行完毕后再加载下一个脚本。
总结:
defer适用于需要在整个页面解析完成后才执行的脚本,通常用于需要依赖页面元素的脚本。async适用于不依赖于页面元素的脚本,可以与其他脚本并行加载和执行。
javascrip类数组对象的定义
一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法,常见的类数组对象有arguments和DOM方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有length属性值,代表可接收的参数个数
常见的类数组转换数组的方法有
Array.prototype.slice.call(arrayLike)
Array.prototype.splice.call(arrayLike,0)
Array.prototype.concat.apply([],arrayLike)
Array.from(arrayLike)
数组有哪些原生方法
- 数组和字符串的转换:toString() toLocalString() join()
- 数组尾部操作的方法:pop() push()
- 数组首部操作的方法:shift() ,unshift()
- 重排序的方法reverse() sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置
- 数组连接的方法concat() 返回的是拼接好的数组,不影响原数组
- 数组截取办法slice(),用于截取数组中的一部分返回,不影响原数组
- 数组插入方法splice()影响原数组查找特定项的索引的方法,indexOf() lastIndexOf() every() some() filter() map() forEach()方法
- 数组归并方法reduce()和reduceRight()方法
unicode utf-8 utf-16 utf-32的区别
常见的位运算符有哪些?其计算规则是什么
为什么函数的arguments参数是类数组而不是数组,如何遍历类数组
和数组类似,但是没有数组的觉见属性和方法,所以叫它们类数组
function foo(){
Array.prototype.forEach.call(arguments, a => console.log(a))
}
function foo(){
const arrArgs = Array.from(arguments)
arrArgs.forEach(a => console.log(a))
}
function foo(){
const arrArgs = [...arguments]
arrArgs.forEach(a => console.log(a))
}
什么是DOM和BOM
Javascript 由三部分构成,ECMAScript,DOM和BOM。根据宿主(浏览器)的不同,具体的表现形式也不尽相同,ie和其他的浏览器风格迥异,IE 扩展了 BOM,加入了 ActiveXObject 类,可以通过 JavaScript 实例化 ActiveX 对象。
-
ECMAScript(核心) 描述了JS的语法和基本对象
-
DOM 是文档对象模型,处理网页内容的方法和接口。是W3C 的标准;
-
BOM 是浏览器对象模型,提供与浏览器交互的方法和接口。各个浏览器厂商根据 DOM在各自浏览器上的实现; 可以参考以下文章写的很全面转发 juejin.cn/post/684490…
对类数组对象的理解,如何转化为数组
类数组对象(Array-like objects)看起来像数组,但实际上并不是真正的数组,它们没有数组的方法和属性。类数组对象通常有一个length属性和一个索引属性的集合。常见的类数组对象包括arguments对象和NodeList对象等
- 使用Array.from()方法:
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const actualArray = Array.from(arrayLike);
console.log(actualArray); // ['a', 'b', 'c']
- 使用扩展运算符(...):
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const actualArray = [...arrayLike];
console.log(actualArray); // ['a', 'b', 'c']
- Array.prototype.slice.call(arrayLike):使用slice方法将类数组对象转换为数组
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const actualArray = Array.prototype.slice.call(arrayLike);
console.log(actualArray); // ['a', 'b', 'c']
- Array.prototype.splice.call(arrayLike, 0, arrayLike.length):使用splice方法将类数组对象转换为数组
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const actualArray = Array.prototype.splice.call(arrayLike, 0, arrayLike.length);
console.log(actualArray); // ['a', 'b', 'c']
escape encodeUrl encodeURLComponent的区别
escape,encodeURI,和encodeURIComponent是用于处理URL的编码和解码的函数。下面详细介绍它们的区别和应用场景。
-
escape:escape是一个内置函数,用于将特殊的字符转换为URL编码。- 它使用的是老的转义序列(即,%XX形式,XX是两位十六进制数)。
- 适用于整个URI(包括路径、查询字符串等部分)。
- 已不建议使用,因为其编码方式在现代浏览器中可能不被支持。
- 应用场景:不建议在现代Web开发中使用。
-
encodeURI:encodeURI是window.encodeURI的简化版本,用于将整个URI进行编码。- 它使用的是新的转义序列(即,%XX形式,XX是两位十六进制数)。
- 适用于整个URI(包括路径、查询字符串等部分)。
- 是
escape的替代品。 - 应用场景:当需要对整个URI进行编码时使用,例如在创建新的URL时。
-
encodeURIComponent:encodeURIComponent是一个内置函数,用于将特殊字符编码为UTF-8的URL编码。- 它使用的是新的转义序列(即,%XX形式,XX是两位十六进制数)。
- 适用于查询字符串或者路径参数中的特殊字符进行编码。
- 与
encodeURI相比,encodeURIComponent只对特定的部分进行编码。 - 应用场景:当需要对查询字符串或者路径参数中的特殊字符进行编码时使用,例如在向服务器发送GET请求时。
总结:
escape已被弃用,建议使用encodeURI或encodeURIComponent。encodeURI适用于对整个URI进行编码,而encodeURIComponent适用于对查询字符串或路径参数中的特殊字符进行编码。
对ajax的理解,实现一个ajax请求
AJAX(Asynchronous JavaScript and XML)是一种技术,通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
以下是使用原生 JavaScript 实现一个简单的 AJAX 请求的步骤:
- 创建一个XMLHttpRequest对象,这是用于在后台与服务器交换数据的关键。
var xhttp = new XMLHttpRequest();
- 定义一个处理HTTP请求的回调函数。当HTTP请求完成后,这个函数将被调用。
javascript复制代码
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// 当请求成功完成时,这个函数将被调用
document.getElementById("demo").innerHTML = this.responseText;
}
};
- 定义HTTP请求的类型、URL以及是否使用异步模式。对于GET请求,可以在URL后面添加参数。
xhttp.open("GET", "https://api.example.com/data?param1=value1¶m2=value2", true);
- 发送HTTP请求。
xhttp.send();
这样,你就完成了一个简单的 AJAX 请求。上述代码将从 https://api.example.com/data?param1=value1¶m2=value2 获取数据,然后在 readyState 为4(请求完成)且 status 为200(成功)时,更新页面上的某个元素(其id为"demo")的内容。
需要注意的是,虽然上述代码可以工作,但在现代的 JavaScript 开发中,我们通常会使用库如 jQuery、axios 等来简化 AJAX 请求的处理。这些库提供了更易用、更强大的 API,并且处理了浏览器间的兼容性问题。
javascript为什么要进行变量提升,它导致了什么问题
JavaScript的变量提升(Hoisting)是指在JavaScript程序执行之前,浏览器会先读取和解析代码,并将所有的变量声明(var)提升到代码的顶部。这意味着即使变量在代码中定义在底部,仍然可以在其声明的上面使用。但是要注意的是,只有变量声明会被提升,赋值语句则不会。
变量提升是为了解决JavaScript中的变量访问问题。在JavaScript中,变量可以在任何地方声明,这可能导致一些问题。例如,如果你在函数内部先使用一个变量,然后才声明它,那么这个变量就会被提升到函数顶部。这可能会导致一些预期外的结果,因为你在使用这个变量之前并没有声明它。
变量提升在JavaScript中是非常有用的,因为它解决了变量声明和赋值的时间顺序问题。通过提升变量声明,我们可以在程序中提前使用变量,稍后再进行赋值。这在很多情况下都非常方便,例如在循环或条件语句中创建临时变量。
然而,变量提升也会导致一些问题。以下是一些可能的问题:
- 重复声明:如果在同一作用域内多次声明同一变量,那么较后面的声明将覆盖前面的声明。这可能会导致难以追踪的错误和难以调试的问题。
- 函数作用域与块作用域混淆:由于变量提升,函数作用域和块作用域的行为可能会产生混淆。例如,在一个函数内部,如果一个变量在函数体内外部都进行了声明,那么外部的变量声明将被提升到函数顶部,而内部的变量声明将覆盖外部的声明。这可能会导致预期之外的行为。
- 变量的预期行为被改变:在一些情况下,变量的预期行为可能会被改变。例如,在一个if语句中,如果条件表达式依赖于一个未声明的变量,那么由于变量提升,条件表达式可能会得到意外的结果。
- 安全性问题:由于变量提升,攻击者可能会利用未声明的变量来执行恶意代码。例如,攻击者可以通过创建一个未声明的全局变量来访问和修改其他全局变量或函数。
因此,尽管变量提升在某些情况下非常有用,但在编写JavaScript代码时,需要注意可能的问题并采取适当的措施来避免这些问题。例如,避免在同一个作用域内重复声明同一变量,使用块作用域来限制变量的作用范围,以及仔细考虑变量的命名和作用域等。
什么是尾调用,使用尾调用有什么好处
尾调用指的是函数的最后一步调用另一个函数。使用尾调用的好处是,由于已经是函数的最后一步,所以不必再保留当前的执行上下文,从而节省了内存。这被称为尾调用优化。在严格模式下,ES6的尾调用优化才会开启,正常模式是无效的。
ES6模块与Commonjs模块有什么异同
- 语法差异:ES6模块使用import和export关键字来导入和导出模块,语法更加简洁和直观。CommonJS使用require()来导入模块,使用module.exports或exports来导出模块。
- 静态与动态:ES6模块是静态的,这意味着模块的依赖关系在编译时就确定了,使得编译器能够进行更好的优化。CommonJS模块是动态的,它在运行时解析模块的依赖关系,因此可能会导致一些运行时的开销。
- 异步加载:ES6模块支持异步加载模块(如动态import()),这使得实现按需加载变得更加容易。CommonJS模块在语言级别不支持异步加载,但在Node.js环境下可以使用特定的函数来实现类似的效果。
- 缓存与实例化:ES6模块的导入是单例模式,也就是说,一个模块只会被加载和执行一次,后续的导入都会返回同一个实例。CommonJS模块的导入是值拷贝,每次导入都会创建一个新的实例。
- 浏览器和Node.js兼容性:ES6模块的兼容性在浏览器中逐渐提升,但在Node.js中还需要使用--experimental-modules标志启用。CommonJS模块在Node.js中是原生支持的,也可以在一些前端构建工具中使用。
- 静态分析:ES6模块的静态分析意味着工具可以在不执行代码的情况下分析模块的依赖关系,这有助于更好的构建和优化。CommonJS模块的动态特性使得静态分析更具挑战性,限制了一些优化能力。
总之,ES6模块和Commonjs模块在语法、静态与动态、异步加载、缓存与实例化、浏览器和Node.js兼容性以及静态分析等方面存在一些异同。在使用时需要根据具体场景选择适合的模块系统。
常见的DOM操作有哪些
-
查找节点:
- document.getElementById('id属性值');返回拥有指定id的第一个对象的引用。
- document.getElementsByClassName('class属性值');返回拥有指定class的对象集合。
- document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合。
- document.getElementsByName('name属性值');返回拥有指定名称的对象结合。
- document/element.querySelector('CSS选择器');仅返回第一个匹配的元素。
- document/element.querySelectorAll('CSS选择器');返回所有匹配的元素。
-
新建节点:
- document.createElement(element);创建新的DOM元素。
-
添加新节点:
- appendChild(newNode) 在现有DOM节点的末尾添加一个新节点。
- insertBefore(newNode, referenceNode) 在现有DOM节点的参考节点之前插入一个新节点。
-
删除节点:
- removeChild(oldNode) 删除现有DOM节点的子节点。
-
设置样式:
- element.style.property = value;设置元素的CSS样式属性。
-
修改 DOM 元素
修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。
将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:
<html>
<head>
<title>DEMO</title>
</head>
<body>
<div id="container">
<h1 id="title">我是标题</h1>
<p id="content">我是内容</p>
</div>
</body>
</html>
现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild
// 获取父元素
var container = document.getElementById('container')
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)
发c
use strict是什么意思,使用它区别是什么
"use strict"是JavaScript中的一个指令,它告诉JavaScript解释器以“严格模式”运行代码。
严格模式是一种更严格的错误处理模式,它对某些容易犯的错误进行了更严格的检查。在严格模式下,一些常见的错误,如引用不存在的变量、使用未声明的变量、删除变量或函数、混淆变量和函数等,都会导致错误。
使用"use strict"的好处包括:
- 避免错误:严格模式可以帮助你避免一些常见的错误,例如引用未声明的变量等。
- 提高性能:严格模式可以优化JavaScript代码的性能,因为某些可能导致错误的代码在严格模式下会被禁止。
- 更好的代码可读性:严格模式强制使用更具可读性的编码风格,例如变量声明必须使用var或let。
然而,使用"use strict"也有一些缺点,例如它会使代码的兼容性变差,因为不是所有的浏览器都支持严格模式。此外,如果你的代码中有一些错误,它们可能会在严格模式下被暴露出来,这可能会导致你的代码无法运行。
因此,在使用"use strict"时,需要权衡其优缺点,并根据实际情况做出决策。
如何判断一个对象是否属于某个类
- 使用
instanceof运算符
instanceof 运算符用于检查一个对象是否是一个构造函数的实例
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
let myCar = new Car('Toyota', 'Corolla', 2020);
console.log(myCar instanceof Car); // 输出 true
- 使用
constructor属性
每个对象都有一个 constructor 属性,它是一个指向创建该对象的构造函数。
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
let myCar = new Car('Toyota', 'Corolla', 2020);
console.log(myCar.constructor === Car); // 输出 true
- 使用
Object.prototype.toString()方法
Object.prototype.toString() 方法可以返回一个表示对象的字符串。我们可以利用这个方法来判断一个对象是否属于某个类。
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
let myCar = new Car('Toyota', 'Corolla', 2020);
console.log(Object.prototype.toString.call(myCar) === '[object Object]'); // 输出 true
Object.prototype.isPrototypeOf()方法
Object.prototype.isPrototypeOf() 方法用于检查一个对象是否是另一个对象的原型。如果一个对象继承自另一个对象的原型,那么 isPrototypeOf() 方法返回 true。
以下是使用 isPrototypeOf() 方法的示例:
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
let myCar = new Car('Toyota', 'Corolla', 2020);
console.log(Car.prototype.isPrototypeOf(myCar)); // 输出 true
这个方法与 instanceof 运算符的功能类似,它们都可以用来判断一个对象是否属于某个类。但是,instanceof 运算符更常用于判断实例对象是否属于某个类,而 isPrototypeOf() 方法更常用于检查一个对象的原型链中是否有某个构造函数。
typeof instanceof isprototype constructor 这些分别的区别和使用场景的例子
typeof, instanceof, isPrototypeOf, 和 constructor 是 JavaScript 中用来检查对象和类的几个关键方法。让我们分别看看它们的作用和示例。
typeof:这个运算符返回一个字符串,表示未经计算的操作数的类型。例如:
console.log(typeof 'Hello'); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"(这是 JavaScript 的一个历史遗留问题)
console.log(typeof {}); // "object"
console.log(typeof function() {}); // "function"
instanceof:这个运算符用来检测构造函数的prototype属性是否出现在对象的原型链中。例如:
function Car() {}
let b = new Car();
console.log(b instanceof Car); // true
isPrototypeOf:这个方法用来检查对象是否是另一个对象的原型。例如:
function Car() {}
let b = new Car();
console.log(Car.prototype.isPrototypeOf(b)); // true
constructor:这个属性返回对创建此对象的数组的构造函数引用。例如:
let b = new Array();
console.log(b.constructor === Array); // true
使用场景的例子:
- 在进行类型检查时,我们通常使用
typeof。例如,在函数参数中,我们可能想要检查一个参数是否是字符串、数字、布尔值等。 - 当我们想要检查一个对象是否是由某个构造函数创建的,我们通常使用
instanceof。例如,在处理用户输入时,我们可能想要确保我们得到的是一个Date对象,而不是一个其他类型的对象。 - 当我们想要检查一个对象是否继承自另一个对象的原型,我们使用
isPrototypeOf。例如,当我们在自定义对象上实现一些方法时,我们可能需要确保这些方法是在正确的原型链上。 - 当我们想要访问一个对象的构造函数时,我们通常使用
constructor。例如,当我们需要创建一个新的对象实例时,我们可能需要调用相应的构造函数。
强类型语言和弱类型语言的区别
强类型语言和弱类型语言的主要区别在于它们的类型系统以及类型转换规则。
强类型语言,如Java、.net、Python、C++等,它们在定义变量时必须显式声明变量的类型。一旦变量被赋予了某种类型,它就不能被隐式地转换为其他类型,除非进行显式的类型转换。强类型语言在速度上可能略逊于弱类型语言,但它们提供的严谨性有助于避免某些错误。
弱类型语言,如VB、PHP、JavaScript等,它们在定义变量时不需要显式声明变量的类型。此外,弱类型变量可以根据环境变化自动进行类型转换,而不需要显式的类型转换。这意味着,例如,如果一个变量被定义为字符串类型,但在后续的代码中用数字进行赋值,该变量将自动转换为数字类型。这种灵活性对于开发人员来说可能更方便,但也可能会导致一些意想不到的错误。
总的来说,强类型语言和弱类型语言的区别在于它们对变量类型的要求和处理方式。强类型语言提供了更高的严谨性和错误检查,而弱类型语言提供了更大的灵活性和便利性。选择哪种类型的语言取决于具体的需求和偏好。
解释性语言和编译型语言的区别
(1)解释型语言 使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下
- 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
- 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
- JavaScript、Python等属于解释型语言。
(2)编译型语言 使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
- 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
- 与特定平台相关,一般无法移植到其他平台;
- C、C++等属于编译型语言。
两者主要区别在于: 前者源程序编译后即可在该平台运行,后者是在运行期间才编译。所以前者运行速度快,后者跨平台性好。
for in和for of的区别
for in 可枚举的
key 和 value
for in 遍历的是键下标,会遍历整个原型链,而 for … of 只遍历当前对象不会遍历原型链
对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
-
数组遍历:
- for in:适用于遍历对象,输出的是数组的index下标。
- for of:适用于遍历数组元素值(value)。
-
对象遍历:
- for in:适用于遍历对象,for in 可以遍历出对象的所有可枚举属性,例如对象、数组、字符串等。
- for of:不能遍历对象,只能遍历带有iterator接口的,例如Set,Map,String,Array。
总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
const arr=[10,20,30]
for(let key in arr){
console.log(key) //0 1 3 (index)
}
for(let val of arr){
console.log(val) //10,20 30 (数值)
}
const str='abc'
for(let c of str){
console.log(c) //a,b,c (数值)
}
function fn(){
for(let arg of arguments){
console.log(arg)
}
}
fn(100,200,'aaa') //100 200 aaa
ajax,axios fetch的区别
数组的遍历方法有哪些
- for循环:这是最基本的方法,用于遍历数组的每个元素。
let arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
- for...of循环:这是ES6引入的新特性,用于直接获取数组中的每个元素。
let arr = [1, 2, 3, 4, 5];
for (let value of arr) {
console.log(value);
}
- forEach方法:该方法用于遍历数组中的每个元素,并对每个元素执行指定的函数。
let arr = [1, 2, 3, 4, 5];
arr.forEach(function(value, index) {
console.log(value);
});
- map方法:该方法与forEach类似,但是它返回一个新数组,该数组包含对原始数组中每个元素执行函数的返回值。
let arr = [1, 2, 3, 4, 5];
let newArr = arr.map(function(value, index) {
return value * 2;
});
console.log(newArr); // [2, 4, 6, 8, 10]
- reduce方法:该方法对数组中的每个元素执行一个函数,将其减少为单个值。这通常用于对数组进行计算或聚合操作。
let arr = [1, 2, 3, 4, 5];
let sum = arr.reduce(function(accumulator, value) {
return accumulator + value;
}, 0);
console.log(sum); // 15
- filter方法:该方法创建一个新数组,其中包含通过提供的函数实现的测试的所有元素。
let arr = [1, 2, 3, 4, 5];
let newArr = arr.filter(function(value) {
return value > 2;
});
console.log(newArr); // [3, 4, 5]
- reduceRight方法:与reduce类似,但是从右到左执行。这在处理需要保留元素插入顺序的右到左操作时非常有用。
let arr = [1, 2, 3, 4, 5];
let sum = arr.reduceRight(function(accumulator, value) {
return accumulator + value;
}, 0);
console.log(sum); // 15 (same as reduce)
- every方法:该方法测试数组的所有元素是否都符合条件。只有所有元素都通过测试,才会返回true。
let arr = [1, 2, 3, 4, 5];
let result = arr.every(function(value) {
return value > 0;
});
console.log(result); // true
- some方法:该方法测试数组中是否有元素符合条件。只要有任何一个元素通过测试,就会返回true。
let arr = [1, 2, 3, 4, 5];
let result = arr.some(function(value) {
return value > 3;
});
console.log(result); // true
- find方法:该方法返回符合测试条件的第一个元素的值。如果没有符合条件的元素,则返回undefined。
let arr = [1, 2, 3, 4, 5];
let result = arr.find(function(value) {
return value > 3;
});
console.log(result); // 4
- findIndex方法:该方法返回符合测试条件的第一个元素的索引。如果没有符合条件的元素,则返回-1。
let arr = [1, 2, 3, 4, 5];
let result = arr.findIndex(function(value) {
return value > 3;
});
console.log(result); // 3
- includes方法:该方法用于确定数组是否包含一个或多个特定的值,根据情况返回true或false。
let arr = [1, 2, 3, 4, 5];
let result = arr.includes(3);
console.log(result); // true
js中哪些数组方法是改变原数组的,哪些是不改变原数据的
改变原始数组的方法:
pop():删除并返回数组的最后一个元素。push():将一个或多个元素添加到数组的末尾,并返回新的长度。shift():删除并返回数组的第一个元素。unshift():将一个或多个元素添加到数组的开头,并返回新的长度。slice():返回一个新的数组,包含从开始到结束(不包括结束)选择的浅拷贝。原始数组不会被改变。reverse():颠倒数组中元素的顺序。sort():对数组元素进行排序,并返回数组。
保持原始数组不变的方法:
length:返回数组的长度。indexOf():返回在数组中可以找到给定元素的第一个索引,如果不存在,则返回-1。lastIndexOf():返回在数组中可以找到给定元素的最后一个索引,如果不存在,则返回-1。forEach():为数组的每个元素执行一次提供的函数。map():创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。filter():创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。reduce():对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。reduceRight():与reduce()类似,但是从右到左执行。find():返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。findIndex():返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1。some():测试数组中是否至少有一个元素通过由提供的函数实现的测试。every():测试数组的所有元素是否都通过了由提供的函数实现的测试。includes():判断一个数组是否包含一个指定的项,根据情况,如果需要返回 true 或 false。join():将数组(或一个类数组对象)的所有元素连接到一个字符串中。concat():合并两个或更多数组,并返回一个新数组。slice():返回一个新的数组,包含从开始到结束(不包括结束)选择的浅拷贝。原始数组不会被改变。join():将数组(或一个类数组对象)的所有元素连接到一个字符串中。slice():返回一个新的数组,包含从开始到结束(不包括结束)选择的浅拷贝。原始数组不会被改变。splice():删除或替换现有元素,或添加新元素。join():将数组(或一个类数组对象)的所有元素连接到一个字符串中。slice():返回一个新的数组,包含从开始到结束(不包括结束)选择的浅拷贝。原始数组不会被改变。
forEach和map方法有什么区别及使用场景
forEach 和 map 是 JavaScript 中用于遍历数组(以及类数组对象)的方法,它们有一些共同之处,但也有一些重要的区别。
forEach
forEach 是一个简单的遍历方法,它会对数组的每个元素执行一次给定的函数。这个函数接收三个参数:元素的值,元素的索引,以及数组本身。
例如:
let arr = [1, 2, 3, 4, 5];
arr.forEach(function(element, index, array) {
console.log(element); // 打印每个元素的值
});
这个例子中,给定的函数会打印出数组中的每个元素。
forEach 的一个重要特点是,它不返回任何值。这意味着你不能直接用 forEach 来修改数组,或者生成新的数组。
map
map 也是一个遍历方法,它会遍历数组的每个元素,对每个元素执行一次给定的函数,并返回一个新数组,新数组的每个元素都是原数组元素执行函数后的结果。
例如:
let arr = [1, 2, 3, 4, 5];
let newArr = arr.map(function(element) {
return element * 2; // 将每个元素乘以2
});
console.log(newArr); // 打印 [2, 4, 6, 8, 10]
这个例子中,给定的函数将每个元素乘以2,然后返回。结果是一个新的数组,其中的元素是原数组元素的两倍。
区别
主要的区别在于 forEach 不返回任何值,而 map 返回一个新的数组。因此,如果你需要修改原始数组,或者生成一个新的数组,你应该使用 map。如果你只是想对数组的每个元素执行一些操作,而不需要保留结果,你可以使用 forEach。
使用场景
- forEach 通常用于需要对数组的每个元素执行一些操作,但不需要保留结果的场景。例如,用于统计数组中所有元素的和,或者检查数组中是否存在某个元素。
- map 通常用于需要将数组的每个元素转换为另一种形式,并保留结果的场景。例如,将数组中的每个元素都乘以2,或者将字符串数组的每个元素都转换为大写。
对对象与数组的解析的理解
在JavaScript中,对象(Object)和数组(Array)都是用来存储数据的集合类型,但它们有一些重要的区别。
对象(Object)
对象是一种无序的数据集合,它由一系列键值对组成。每个键都是一个字符串,每个值可以是任何JavaScript中的数据类型(例如,其他对象、函数、基本类型等)。
javascript复制代码
let obj = {
name: 'John',
age: 30,
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
};
在上面的例子中,name、age和sayHello都是对象的属性。name和age是基本类型的属性,sayHello是一个函数类型的属性。通过使用点(.)或方括号([])语法,我们可以访问这些属性。
数组(Array)
数组是有序的数据集合,它由一系列元素组成,每个元素都可以通过索引访问。数组在JavaScript中是一种特殊的对象,它有一些特殊的属性和方法,比如length属性(返回数组的长度)和push()、pop()等方法(向数组末尾添加或删除元素)。
let arr = [1, 2, 3, 4, 5];
console.log(arr[0]); // 输出:1
console.log(arr.length); // 输出:5
在上面的例子中,我们创建了一个包含五个元素的数组,然后通过索引0访问了第一个元素,通过length属性访问了数组的长度。
一些重要的区别:
- 顺序:数组的元素是有顺序的,可以通过索引访问,而对象的属性是无序的,无法通过索引访问。
- 键的类型:在对象中,键必须是字符串或Symbol。在数组中,索引必须是数字。
- 添加元素:在对象中,我们可以通过点或方括号语法直接添加新的键值对。在数组中,我们通常使用push()方法添加元素。
- 删除元素:在对象中,我们无法直接删除属性。在数组中,我们可以使用splice()方法删除元素。
- 遍历:在对象中,我们可以使用for-in循环或Object.keys()方法遍历所有的属性。在数组中,我们可以使用for循环、for-in循环或forEach()方法遍历所有的元素。
如何提取高度嵌套的对象里的指定属性
要提取高度嵌套的对象中的指定属性,您可以使用递归来遍历对象并提取所需的属性。以下是一个示例函数,它接受两个参数:要提取的属性名称和要搜索的对象。
function extractNestedProperty(prop, obj) {
let result = obj;
// 递归遍历对象
for (let part of prop.split('.')) {
if (result[part] !== undefined) {
result = result[part];
} else {
return undefined;
}
}
return result;
}
const nestedObj = {
a: {
b: {
c: 123,
d: {
e: 456,
f: {
g: 789
}
}
},
h: {
i: {
j: 'abc'
}
}
}
};
console.log(extractNestedProperty('a.b.c', nestedObj)); // 输出:123
console.log(extractNestedProperty('a.b.d.e', nestedObj)); // 输出:456
console.log(extractNestedProperty('a.h.i.j', nestedObj)); // 输出:'abc'
console.log(extractNestedProperty('x.y', nestedObj)); // 输出:undefined
该函数将属性名称拆分为多个部分,并逐个访问对象的属性。如果属性存在,它将进一步深入嵌套对象,否则返回 undefined。您可以根据需要调整该函数来满足您的具体要求。
对rest参数的理解
Rest参数是一种在函数中获取多余参数的方式,它允许函数接收任意数量的参数。Rest参数在ES6中引入,其形式为"...变量名",并且只能作为函数的最后一个参数。
Rest参数可以看作是一个真正的数组,因此可以使用数组的任何方法。当函数有多个参数时,Rest参数会收集所有未命名的参数。这意味着Rest参数不仅可以接收函数的多余参数,而且还可以接收到函数中没有明确命名的参数。
Rest参数和arguments对象的区别在于,
Rest参数:
- Rest参数只包括那些没有给出名称的参数,
- 而Rest参数是数组实例,可以直接应用数组的方法。
arguments
- arguments包含所有参数。
- arguments对象不是真正的数组,
Rest参数也具有一些限制,例如它必须是最后一个参数,在它之后不能存在任何其他的参数,否则会报错。此外,函数的length属性不包含Rest参数。
Rest参数可以用于解构赋值,即将Rest参数的数据解析后一一对应到相应的变量中。需要注意的是,Rest参数必须用[]括起来,因为它是数组。
ES6中模板语法与字符串处理
在ES6(ECMAScript 6)中,模板字符串(template literals)和字符串的某些处理方法,如includes(), startsWith(), endsWith(), repeat(), 和 padStart() 等,都得到了显著的增强,使字符串操作更加方便。
模板字符串(Template Literals)
在ES6之前,JavaScript使用反引号()创建字符串,如果反引号中包含变量,则需要通过+`连接变量和字符串,例如:
var name = "John";
var greeting = "Hello, " + name + "!";
而在ES6中,我们可以通过使用反引号(`)创建模板字符串,直接将变量插入字符串中,例如:
var name = "John";
var greeting = `Hello, ${name}!`;
在模板字符串中,我们可以在字符串中使用 ${} 插入变量。这种新的字符串字面量提供了一种更简单、更直观的方式来创建包含变量的字符串。
字符串处理方法
以下是一些在ES6中新增的字符串处理方法:
includes(): 检查字符串是否包含指定的子字符串。例如:
let string = 'Hello, World!';
console.log(string.includes('World')); // 输出: true
startsWith(): 检查字符串是否以指定的字符串开始。例如:
let string = 'Hello, World!';
console.log(string.startsWith('Hello')); // 输出: true
endsWith(): 检查字符串是否以指定的字符串结束。例如:
javascript复制代码
let string = 'Hello, World!';
console.log(string.endsWith('!')); // 输出: true
repeat(): 重复字符串指定的次数。例如:
let string = 'Hello, World!';
console.log(string.repeat(3)); // 输出: 'Hello, World!Hello, World!Hello, World!'
padStart(): 在当前字符串的开始添加指定的填充字符串,直到达到给定的长度。例如:
let string = '5';
console.log(string.padStart(10, '0')); // 输出: '0000000005'
这些新的字符串处理方法大大增强了JavaScript处理字符串的能力,提供了更方便、更强大的工具来操作和处理字符串。
垃圾回收与内存泄漏
浏览器的垃圾回收机制
浏览器的垃圾回收机制是一种自动管理内存的方式,它能够避免内存泄漏并回收不再使用的内存。这个过程主要依赖于两种实现方式:标记清除和引用计数。
- 标记清除:这是最常用的垃圾回收方式。它首先从根节点(Root)开始,遍历所有的对象。可以遍历到的对象被标记为可达(reachable),没有被遍历到的对象则被标记为不可达(unreachable)。然后,垃圾收集器会统一清理内存中所有被标记为不可达的对象所占据的内存。
- 引用计数:这是一种相对简单的垃圾回收方式。每当一个对象被引用时,它的引用计数就会增加;每当一个对象被丢弃时,它的引用计数就会减少。当一个对象的引用计数变为零时,就意味着这个对象不再被使用,因此可以将其回收。
在浏览器的JavaScript中,垃圾回收机制是自动运行的,它按照一定的时间间隔周期性地执行。这可以帮助程序员自动管理内存,避免内存泄漏。
哪些情况会导致内存泄漏
- 对象被存储但不再需要:如果一个对象被存储在内存中,但不再需要,而没有及时释放,就会导致内存泄漏。
- 对象被存储为全局变量:如果一个对象被存储为全局变量,那么即使在程序的其他地方不再需要它,它也会一直存在于内存中,从而导致内存泄漏。
- 没有正确释放内存:如果在程序中没有正确释放内存,也会导致内存泄漏。例如,使用malloc()函数分配内存后,没有使用free()函数释放内存。
- 循环引用:如果两个或多个对象相互引用,并且没有被其他对象引用,就会导致循环引用。这种情况下,即使这些对象不再需要,它们也会一直存在于内存中,从而导致内存泄漏。
- 数据库连接未关闭:如果在使用数据库时没有正确关闭数据库连接,就会导致内存泄漏。
- 代码逻辑错误:有时候,代码逻辑错误也可能导致内存泄漏。例如,在程序中错误地使用了指针或数组,或者在删除对象后再次使用了该对象的指针。