• JavaScript常见面试题(三)


    1.对原型、原型链的理解

    原型:是一个对象,是函数的一个属性prototype(任何函数都有),通过该函数实例化出来的对象都可以继承得到原型上的所有属性和方法。
    原型链:访问实例对象的属性时,首先在自己身上查找,如果自身不存在,那么就去它的原型对象(prototype属性)上查找,这个原型对象又有自己的原型,直到找到Object原型(为null),以此形成的类似链条的结构就称为原型链。

    • 对象的proto保存着该对象的构造函数的prototype

    2.原型修改、重写

    • 原型的修改,修改以后对象所有的实例都可以使用新增的方法
    function Person(name) {
        this.name = name
    }
    // 修改原型
    Person.prototype.getName = function() {}
    
    • 重写,用一个新的对象来替换原来的原型。
    Person.prototype = {
        getName: function() {}
    }
    

    注意:重写原型时,对象的构造函数可能会指向根构造函数Object

    Person.prototype = {
        getName: function() {}
    }
    var p = new Person('hello')
    p.constructor = Person
    console.log(p.__proto__ === Person.prototype)        // true
    console.log(p.__proto__ === p.constructor.prototype) // true
    

    3.原型链指向

    function Person(name) {
      this.name = name
    }
    
    var p = new Person('John')
    var p1 = new Person('Mike')
    
    console.log(p.__proto__) // Person.prototype
    console.log(Person.prototype.__proto__) // Object.prototype
    console.log(p.__proto__.__proto__) // Object.prototype
    console.log(p.__proto__.constructor.prototype.__proto__) // Object.prototype
    console.log(Person.prototype.constructor.prototype.__proto__) // Object.prototype
    console.log(p1.__proto__.constructor) // Person
    console.log(Person.prototype.constructor) // Person
    

    4.对闭包的理解

    概念:闭包的含义是一个函数有权访问另一个函数作用域的函数(A函数中创建B函数,函数B可以以访问到函数A中的变量,函数B就是闭包)。
    用途:在函数外部能够访问到函数内部的变量;函数运行结束以后会保留对这个变量对象的引用,不会回收对象。
    不足:占用更多内存,会引起内存泄漏。
    应用场景:防抖节流–保存上一次运行的setTimeout(不用闭包会重新创建定时器,获取不到上一次定时器时间);函数作为返回值;vue的响应式原理;函数嵌套;

    5. 对作用域、作用域链的理解

    概念:作用域–>变量或者是函数能作用的范围。
    作用域链:当使用一个变量时,首先在当前作用域查找,如果没找到,就去它的上层作用域去查找,直到找到或者到了全局作用域,这样形成的链式查找称为作用域链。
    作用域分为函数作用域,全局作用域以及块级作用域。
    全局作用域:常常定义在函数外部,全局作用域变量可以在任意位置访问。
    局部作用域(函数作域):定义在函数内部,只能在函数中使用的变量,作用范围是从函数开始到结尾。
    块级作用域:ES6 提供 let & const 变量实现块级作用域。
    块级作用域应用场景:内部变量会覆盖外部变量(用来计数的循环变量泄漏为全局变量)。
    作用:作用域链的作用是保证对执行环境有权访问的所有变量的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

    6.对执行上下文的理解

    执行上下文分为三种:
    全局执行上下文:只有一个,程序首次运行时创建,它会在浏览器中创建一个全局对象(window对象),使this指向这个全局对象。
    函数执行上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文。
    Eval 函数执行上下文。

    • 执行上下文:在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
    • 执行上下文栈:JavaScript引擎使用执行上下文栈来管理执行上下文。

    7.对this对象的理解

    概念:this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。
    指向
    函数调用:指向全局对象window
    方法调用:指向这个方法的对象
    构造器调用:如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
    apply 、 call 和 bind 调用:这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。

    8. call() 和 apply() 的区别?

    都是改变函数this指向的方法

    • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
    • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

    9.异步编程的实现方式?

    同步是指一个任务完成后才能执行另一个任务。
    异步是把一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。排在异步任务后面的代码,不用等待异步任务结束会马上运行。
    异步编程的方法(JS 异步编程进化史:callback -> promise -> generator -> async + await)

    1. 回调函数(回调函数是一个函数,它被作为参数传递给另一个函数(通常称为“主调函数”或“高层函数”)。当主调函数完成某些操作或满足特定条件后,它会调用这个回调函数。):容易造成回调地狱问题
    2. Promise:有时会造成多个 then 的链式调用
    3. 生成器Generators/ yield
    4. async/await:async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。

    10.setTimeout、Promise、Async/Await 的区别

    • setTimeout
    console.log('script start')	//1. 打印 script start
    setTimeout(function(){
        console.log('settimeout')	// 4. 打印 settimeout
    })	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
    console.log('script end')	//3. 打印 script start
    // 输出顺序:script start->script end->settimeout
    
    • Promise : Promise本身是同步的立即执行函数,当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行
    console.log('script start')
    let promise1 = new Promise(function (resolve) {
        console.log('promise1')
        resolve()
        console.log('promise1 end')
    }).then(function () {
        console.log('promise2')
    })
    setTimeout(function(){
        console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
    
    • async/await:await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。
    async function async1(){
       console.log('async1 start');
        await async2();
        console.log('async1 end')
    }
    async function async2(){
        console.log('async2')
    }
    console.log('script start');
    async1();
    console.log('script end')
    // 输出顺序:script start->async1 start->async2->script end->async1 end
    

    11.对Promise的理解

    Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调。
    Promise的实例有三个状态:Pending(进行中),Resolved,Rejected

    • Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。
    • 实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
    • 在构造 Promise 的时候,构造函数内部的代码是立即执行的

    12.Promise的基本用法

    • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
    const promise = new Promise(function(resolve, reject) {
      // ... some code
      if (/* 异步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });
    
    • 一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法
    Promise.resolve(11).then(function(value){
      console.log(value); // 打印出11
    });
    

    方法

    1. then(),then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。返回值根据回调函数的结果
    2. catch() 该方法相当于then方法的第二个参数,指向reject的回调函数。
    3. all() 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。返回的是一个数组,保存着每一个promise对象resolve执行时的值。失败返回最先失败的值
    4. race 接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。
    5. finally 不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

    13.Promise解决了什么问题

    使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:

    let fs = require('fs')
    fs.readFile('./a.txt','utf8',function(err,data){
      fs.readFile(data,'utf8',function(err,data){
        fs.readFile(data,'utf8',function(err,data){
          console.log(data)
        })
      })
    })
    
    • 后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。
    • 如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。

    14.Promise.all和Promise.race的区别的使用场景

    使用场景
    遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决
    当要做一件事,超过多长时间就不做了,可以用这个race方法来解决

    15.对async/await 的理解

    • async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。
    • async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
    • async 函数返回的是一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过Promise.resolve() 封装成 Promise 对象。

    16.await 到底在等啥?

    await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值。
    await 表达式的运算结果取决于它等的是什么。

    • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
    • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

    17.async/await的优势

    单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

    18.async/await对比Promise的优势

    • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
    • 错误处理友好,async/await可以用成熟的try/catch,Promise的错误捕获非常冗余
    • 方便调试

    19.对象创建的方式有哪些?

    • object构造函数创建(new一个Object)
    //创建 Object 对象
    var p = new Object()
    
    //动态添加属性和方法
    p.name = 'Tom'
    p.age = 18
    p.setName = function(name){
    	this.name = name 
    }
    
    • 对象字面量模式
    var p = {
    	name = 'Tom'
    	age = 18
    	setName: function(name){
    		this.name = name
    	}
    }
    
    • 自定义构造函数:与object构造函数模式类似,通过自定义一个构造函数,再进行new操作创建对象
    function Person(name, age){
    	this.name = name
    	this.age = age
    	this.setName = function(name){
    		this.name = name
    	}
    }
    
    var p1 = new Person('Tom',18)
    
    • 构造函数+原型的组合模式:自定义构造函数,属性在函函数中初始化,方法添加到原型上。
    function Person(name, age){
    	this.name = name
    	this.age = age
    }
    
    Person.prototype.setName = function(name){
    	this.name = name
    }
    
    var p1 = new Person('Tom',18)
    

    20.对象继承的方式有哪些?

    一个对象从另一个对象中继承属性,达到的效果是在一个对象中可以使用另一个对象中定义的属性。

    • 原型链继承:将子类型的原型对象设置为父类型的实例。存在共享引用类型属性的问题。如果在子类型中修改了引用类型的属性,会影响到所有子类型的实例。
    Dog.prototype = new Animal('Tommy')
    
    • 构造函数继承:在子类型的构造函数内部调用父类型的构造函数;解决了不能向超类型传递参数的问题,但无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
    function Employee(name, position) {
      Person.call(this, name) // 在子类型的构造函数内部调用父类型的构造函数,传入name参数
      this.position = position
    }
    
    • 组合继承
    // 子类型
    function Dog(name, age) {
      Animal.call(this, name) // 借用构造函数继承属性
      this.age = age
    }
    
    // 将子类型的原型设置为父类型的实例
    Dog.prototype = new Animal()
    

    组合继承是将原型链继承和借用构造函数继承相结合的一种继承方式,通过这种方式可以解决原型链继承和借用构造函数继承各自的缺点。
    组合继承通过借用构造函数来继承属性,通过将子类型的原型设置为父类型的实例来继承方法。

    • 原型式继承:原型式继承是一种基于已有对象创建新对象的继承方式,适用于简单对象的继承。在 JavaScript 中可以使用 Object.create 方法来实现原型式继承。
    // 基于原型对象创建新对象
    var anotherPerson = Object.create(person)
    
    • 寄生式组合继承:了寄生式继承和组合继承的优点,避免了调用两次父类构造函数以及在子类原型中创建不必要的属性。通过创建一个中间对象来继承父类的原型,而不直接调用父类构造函数。
    function inheritPrototype(subType, superType) {
      var prototype = Object.create(superType.prototype) // 创建对象
      prototype.constructor = subType // 增强对象
      subType.prototype = prototype // 赋值对象
    }
    
    // 子类型
    function Dog(name, age) {
      Animal.call(this, name) // 继承属性
      this.age = age
    }
    
    // 使用寄生式继承来继承父类型的原型
    inheritPrototype(Dog, Animal)
    

    21.浏览器的垃圾回收机制

    JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

    • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束。
    • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放
    • 当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

    垃圾回收方式

    • 标记清除(常用):当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
    • 引用计数法:踪记录每个变量值被使用的次数,当变量值引用次数为0时,垃圾回收机制就会把它清理掉。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。

    22.哪些情况会导致内存泄漏

    • 意外的全局变量:一直留在内存中。
    • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
    • 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 相关阅读:
    vue组件
    .NET 开源项目推荐之 直播控制台解决方案 Macro Deck
    RPA机器人维护的3个注意点
    计算机毕业设计JavaH5女娲宫旅游网站设计与实现(源码+系统+mysql数据库+lw文档)
    4-7再谈方法之方法参数的值传递(练习)
    exec()和eval()
    中国联通5G-NR 900MHz基站设备技术白皮书(2022)
    基于springboot+vue的在线购房(房屋租赁)系统
    论文精读NMP:Neural Map Prior for Autonomous Driving
    easyrecovery工具2023最新版一键恢复丢失数据免费下载
  • 原文地址:https://blog.csdn.net/m0_56685529/article/details/139797074