美文网首页
前端面试题js-【持续更新】

前端面试题js-【持续更新】

作者: 林不羁吖 | 来源:发表于2022-05-22 21:35 被阅读0次

一、数据类型

二、ES6

1. let、const、var的区别

(1)块级作用域:块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升:var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

(4)重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向:let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

2.const对象的属性可以修改

  • 保存的数据一旦被赋值,就不能被修改
  • 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容

3.如何在ES5环境下实现let

因为es6中提出了let,所以现在开发中除了定义常量都是使用let,比如说在for循环中使用let,如果把一段es6的for循环代码转化为es5的,我发现

//原代码
for (let index = 0; index < 10; index++) {
    console.log(`index`, index)

}
console.log(index)
//转es5
"use strict";

for (var _index = 0; _index < 10; _index++) {
    console.log("index", _index);
}
console.log(index);

babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,除了对变量名的转换,我们也可以通过自执行函数来模拟块级作用域

(function(){
  for(var i = 0; i < 5; i ++){
    console.log(i)  // 0 1 2 3 4
  }
})();

console.log(i)      // Uncaught ReferenceError: i is not defined

4.如何在ES5环境下实现const

实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:Object.defineProperty(obj, prop, desc)

参数 说明
obj 要在其上定义属性的对象
prop 要定义或修改的属性的名称
descriptor 将被定义或修改的属性描述符
属性描述符 说明 默认值
value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined undefined
get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined undefined
set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法 undefined
writable 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false false
enumerable enumerable定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举 false
Configurable configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改 false

对于const不可修改的特性,我们通过设置writable属性来实现

function _const(key, value) {    
    const desc = {        
        value,        
        writable: false    
    }    
    Object.defineProperty(window, key, desc)
}
    
_const('obj', {a: 1})   //定义obj
obj.b = 2               //可以正常给obj的属性赋值
obj = {}                //无法赋值新对象

三、JavaScript基础

3.1 执行上下文

3.2 作用域链

3.3 闭包

3.4 this

3.5 call/apply/bind

七、异步编程

7.1 promise

八、面向对象

8.1 js基础~instanceof原理

a instanceof Foo

字面解释的话是判断构造函数Foo的原型对象是否在对象a的原型链上,一般用于判断实例是否属于某一种类型

8.2 谈谈你对原型对象的理解

在我看来原型对象分为隐式原型和显式原型。

1.隐式原型

每个对象中都有一个__proto__ 的属性,一般叫做对象的隐式原型。

这个隐式原型的作用是:当我们从一个对象中获取某一个属性时, 它会触发 [[get]] 操作

  1. 在当前对象中去查找对应的属性, 如果找到就直接使用
  2. 如果没有找到, 那么会沿着它的隐式原型去查找

【追问】我们可以通过两种方式获取对象的隐式原型

  • 方式一:通过对象的 __proto__属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问

题);

  • 方式二:通过 Object.getPrototypeOf 方法可以获取到;

2.显式原型

每一个函数都有一个 prototype属性,一般叫做函数的显式原型。

这个显式原型的作用是:当我们实例化一个对象时,比如说构造函数Person,

通过Person构造函数创建出来的所有对象的隐式原型都指向Person的显式原型,这样就形成了原型链,达到了复用属性的效果。

8.3 谈谈你对原型链的理解

原型链是一种机制,这种机制将构造函数,原型对象,实例对象联系起来,基于这种机制可以实现继承。

原型链中最关键的几个关系是:

  1. 每个对象都有__proto__属性,该属性指向其构造函数的显式原型对象,在调用实例的方法和属性时,如果在实例对象上找不到,就会往原型对象上找
  2. 构造函数的prototype属性也指向实例的显式原型对象
  3. 原型对象的constructor属性指向构造函数

【追问】从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取。那么什么地方是原型链的尽头呢?

顶层原型(所有类的父类)。从Object直接创建出来的对象的原型都是 [Object: null prototype] {}。

特点:

  • 该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;

  • 该对象上有很多默认的属性和方法;

8.4 谈谈你对构造函数的理解

构造函数也称之为构造器,通常是我们在创建对象时会调用的函数;

  • 构造函数也是一个普通的函数,从表现形式来说,和普通的函数没有任何区别;

  • 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;

8.5 如何创建一个对象

创建对象的方式有很多。

1.字面量和new Object

创建同样的对象时,需要编写重复代码,为了解决这个问题可以采用工厂模式

2.工厂模式

实现方式是在函数中使用new Object,然后根据入参给创建的对象赋值,最后返回对象。

工厂方法创建对象有一个缺点:对象的类型都是Object类型,缺少对象的具体类型,为了解决该问题可以采用构造函数

3.构造函数

构造函数也是一个普通的函数,从表现形式来说,和普通的函数没有任何区别,但是如果用new操作符来调用了,那么这个函数就是一个构造函数了。

构造函数的缺点是:我们需要为每个对象的函数去创建一个函数对象实例;

4.原型

因为在对象中去查找属性时,如果找不到, 就会沿着它的原型去查找,所以把将对象的函数放到够赞函数的显式原型对象上。

最终用构造函数+原型的方式创建对象,可以解决以上的所有问题

8.6 new运算符原理

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

function objectFactory() {

  let newObject = null;  // 创建一个新对象

  let constructor = Array.prototype.shift.call(arguments);  // 是为了要取出传入的构造函数 Function F

  /**  constructor 为

    * function F(name){

        this.name = name;

        this.age = 18;

      }

  */

  let result = null;

  // 判断参数是否是一个函数

  if (typeof constructor !== "function") {

    console.error("type error");

    return;

  }

  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
// Object.create(),接受一个参数作为返回对象的原型

  newObject = Object.create(constructor.prototype);

  // console.log(newObject.__proto__ === constructor.prototype); //true



  // 将 this 指向新建对象,并执行函数

  // ↓ 之前 constructor 为 F()this 指向之后为 F (name: 'gdy', age: 18 )↓

  result = constructor.apply(newObject, arguments);  //argument 'gdy'

  // console.log(constructor.toString());

  // 判断返回对象

  let flag = result && (typeof result === "object" || typeof result === "function");

  // 判断返回结果

  return flag ? result : newObject;

}

// 使用方法

function F(name){

  this.name = name;

  this.age = 18;

}

let newf = objectFactory(F,'gdy');

console.log(newf.name); // 'gdy'

8.7 实现继承的方法

1.原型链继承:

实现方式:子类的原型对象指向父类的实例

  • 1.优点

    1.简单:B.prototype = new A
    2.父类原型原型对象中增加的属性和方法在子类中都能访问的到

  • 2.缺点
    1.为子类增加方法,必须在B.prototype=new A之后
    2.属性和方法都是共享的,会引起混乱
    3.无法实现多继承。
    4.无法传参。

2.构造函数式继承

  • 1.实现方法
    用call和apply在子类的构造函数中运行父类的构造函数,一般使用A.apply(this,arguments)、
  • 2.优点
    1.可以实现多继承(call或apply多个父类)
    2.解决了共享问题
    3.可以传参。
  • 3.缺点
    1.实例只是子类的实例,不是父类的实例。无法被instanceof和isPropertyOf识别。
    2.只能继承构造函数内的属性和方法,不能继承原型对象上的属性和方法。
    3.方法都是在构造函数中运行的,无法复用。

3.组合式继承

同时使用原型继承和借用构造函数继承两种方式。在子类的构造函数内使用call或apply调用父类,并且子类的prototype指向某一个父类的实例。

  • 1.优点
    结合了原型继承和借用构造函数继承两种方式的优点,可以传参,可以多继承,可以使用父类构造函数的方法和属性,也可以使用父类原型对象上的方法和属性。同时也可以被instanceof和isPropertyOf识别
  • 2.缺点
    1.父类被调用了两次,第一次是在使用call或apply的时候,第二次是在将子类的原型对象指向父类的实例的时候。(效率降低)
    2.父类构造函数内所设置的属性或方法在子类的实例和原型对象中各存在了一份,由于同名覆盖的原因,使用时并不受影响。但是会占用额外的内存空间。(浪费空间)

4.原型式继承

三种方式

var obj = {
  name: "why",
  age: 18
}

 var info = Object.create(obj) //1

// 原型式继承函数
function createObject1(o) { //2
  var newObj = {}
  Object.setPrototypeOf(newObj, o)
  return newObj
}

function createObject2(o) {//3
  function Fn() {}
  Fn.prototype = o
  var newObj = new Fn()
  return newObj
}

// var info = createObject2(obj)
console.log(info)
console.log(info.__proto__)

5.寄生式继承

结合原型类继承和工厂模式,可以复用扩展属性的逻辑

4,5缺点:不是实现两个构造函数的继承而是实现两个对象的继承

6.组合寄生式继承(完美继承)

通过将子类原型指向父类实例改为子类原型指向父类原型的浅拷贝来实现,解决了组合式继承的缺点。

九、v8引擎

9.1 V8如何执行一段JS代码

img

1.什么是v8引擎

一种将js代码转化为cpu指令的js引擎

2 为什么用v8执行js代码

当我们编写了js代码想要交给cpu去执行的时候,如果把js代码直接放在cpu里面,cpu是无法识别的,因为cpu只能识别0101这样的机器语言,而js代码是高级语言,所以js代码如果想要执行就需要借助v8引擎。

3 v8执行js代码的执行

  • 1.解析js代码生成AST:经过编码转化,词法分析以及预编译
    https://astexplorer.net/、

    【追问】为什么要进行预解析?

    • 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;

    • 所以V8引擎就实现了延迟解析的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;

    • 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;

  • 2.将AST转化为字节码: 基线编译器(Ignition)将AST转换成字节码。

    【追问】:为什么不直接转成0101机器语言?

    因为无法确定这个代码会运行在怎样的环境上(windows,mac,linux),不同环境的cpu架构不同,不同cpu架构能执行的机器指令不同,无法确定机器指令,所以才转化为字节码,字节码可以跨平台,转化为机器指令后就可以运行了。

  • 3.将字节码转化为机器码:优化编译器(Turbofan)将字节码转换成优化过的机器码,此外在逐行执行字节码的过程中,如果一段代码经常被执行,那么V8会将这段代码直接转换成机器码保存起来,下一次执行就不用经过字节码的转化,优化了执行速度。

【补充】
有一个问题,比如说保存了高频率函数

function sum(num1,num2){
  return num1+num2
} 
sum(20,20) 
sum(30,30) 
sum('aa','bb').

当优化的机器码发现执行指令不同时(数值相加变成了字符串拼接),会进行一个操作deoptimzation,反优化到字节码后再转化成运行结果。
这样会消耗性能,所以在写代码的时候使用typescript,有一个类型的限制,效率会更高一些

9.2 介绍一下引用计数和标记清除

引用计数:

  • 当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;
  • 弊端:会产生循环引用

标记清除:【广泛采用】

  • 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象,然后被销毁掉。
  • 这个算法可以很好的解决循环引用的问题;

相关文章

网友评论

      本文标题:前端面试题js-【持续更新】

      本文链接:https://www.haomeiwen.com/subject/zltyurtx.html