前端面试之JS基础概念快速版

99 阅读13分钟

HTML、CSS、JavaScript、Vue、React、浏览器原理、计算机网络、前端性能优化、手写代码、前端工程化、代码输出结果。

作用域

作用域表示一个区间,在这个区间内声明的所有内容(比如方法或变量)都可以被该区间内的代码访问到。

作用域链

作用域链是一个独特空间。当一个变量被调用,那么变量在 被调用 时所在的局部作用域和全局作用域之间,就形成了一个作用域链。

JS数据类型

基本数据类型:String , Number , Boolean , null , undefined , Symbol

引用数据类型:Object , Array , Function

基本数据类型的值是存放在栈内存中的,而引用数据类型的值是存放在堆内存中的,栈内存中仅仅存放着它在栈内存中的引用(即它在堆内存中的地址),就和它的名字一样,引用数据类型

内存访问机制

基本数据类型可以直接从栈内存中访问变量的值,而引用数据类型要先从栈内存中找到它对应的引用地址,再拿着这个引用地址,去堆内存中查找,才能拿到变量的值。当我们在对引用类型赋值的时候,复制的是该对象的引用地址。

浅拷贝

1、Object.assign 会拷贝所有的属性值到新的对象中,当子属性值是对象的话,拷贝的是仍然是引用。

2、扩展运算符也同样可以达到浅拷贝的作用

浅拷贝只能解决第一层的问题,当子属性是对象的时候,那么,就又回到上面的问题了,两者享有共同的引用地址。

深拷贝

1、Object.assign(target, source1, source2)

es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。

2、JSON.parse(JSON.stringify())

弊端:会忽略 undefined;会忽略 symbol;不能序列化函数;不能解决循环引用的对象;

3、普通递归函数实现深拷贝

function deepClone(source) {
  if (typeof source !== 'object' || source == null) {
    return source;
  }
  const target = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (typeof source[key] === 'object' && source[key] !== null) {
        target[key] = deepClone(source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}

垃圾回收机制

当一个变量不被需要了以后,它就会把这个变量所占用的内存空间所释放,这个过程就叫做垃圾回收

引用计数法

变量引用的次数,你可以认为它就是对当前变量所引用次数的描述。

var obj={name:'jack'};

当我们在给 obj 赋值的同时,其实就创建了一个指向该变量的引用,引用计数为1,在引用计数法的机制下,内存中的每一个值都会对应一个引用计数

而当我们给 obj 赋值为 null时,这个变量就变成了一块没用的内存,那么此时, obj 的引用计数将会变成 0所有引用计数为0的变量都将会被垃圾收集器所回收,那么, obj 所占用的内存空间将会被释放。

标记清除法

它采用的判断标准是看这个对象是否可抵达,主要分为两个阶段,标记阶段清除阶段:

  • 标记阶段

垃圾收集器会从根对象(Window对象)出发,扫描所有可以触及的对象,这就是所谓的可抵达

  • 清除阶段

在扫描的同时,根对象无法触及(不可抵达)的对象,就是被认为不被需要的对象,就会被当成垃圾清除

内存泄漏

该释放的内存垃圾没有被释放,依然霸占着原有的内存不松手,造成系统内存的浪费,导致性能恶化,系统崩溃等严重后果,这就是所谓的内存泄漏

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。因为作用域链的存在,所以内部函数才可以访问外部函数中定义的变量,而不能直接访问其他函数内的变量和方法。闭包的变量会常驻内存,滥用闭包容易造成内存泄漏。

柯里化

柯里化是一种函数的转换,它是指将一个接受n个参数的函数转换为n个只接受一个参数的函数的过程。 柯里化不会调用函数。它只是对函数进行转换。柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。

闭包应用-防抖,节流

防抖

将多次执行变为最后一次执行或立即执行,你可以理解为防止手抖

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染

节流

将多次执行变为每隔一段时间执行一次

  • 滚动加载,加载更多或滚到底部监听

单线程的JS

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

JS同步与异步

同步

任务从上往下按顺序执行,后一个任务必须等待前一个任务执行完

异步

前一个任务还没执行完, 也没关系,直接执行下一个任务

同步任务就是按顺序执行,从上往下。

异步任务里是从上往下,微任务比宏任务先执行。

  • 值得注意的是, Promise 是有一点特殊性的,因为Promise构造函数中函数体的代码都是立即执行的 , 而 Promise.then() 和 Promise.catch() 属于微任务,也就是 resolve() 和 reject()
console.log(1)

setTimeout(function() {
  console.log(2)
})

new Promise(function (resolve) {
  console.log(3)
  resolve()
}).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)
})

console.log(6)

136452

函数

函数是可以执行的 javascript 代码块。

function fn(){
  //to do something
}

方法

通过对象调用的 javascript 函数

obj = {
  fn(){
    //to do something
  }
}

方法和函数类似,同样封装了独立的功能,但是方法是只能依靠类或者对象来调用的,表示针对性的操作。

构造函数

构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。

  • 构造函数用于创建某一类对象,其首字母要大写
  • 构造函数要和 new 一起使用才有意义

new 操作符,到底做了哪些事情

  • 在内存中创建一个新的空对象。
  • 让 this 指向这个新的对象。
  • 执行构造函数里面的代码,给这个新对象添加属性和方法。
  • 返回这个新对象(所以构造函数里面不需要 return )

实现一个 new 方法

// const xxx = _new(Person,'cooldream',24)  ==> new Person('cooldream',24)
function _new(fn,...args){
    // 新建一个对象 用于函数变对象
    const newObj = {};
    // 将空对象的原型地址 `_proto_` 指向构造函数的原型对象
    newObj.__proto__ = fn.prototype;
    // this 指向新对象 
    fn.apply(newObj, args);
    // 返回这个新对象
    return newObj;
}

每一次 new 操作,都会开辟新的内存,所以每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

原型

function Person(){

}

let person = new Person();

构造函数 Person 和 实例对象 person

  • 每一个构造函数都有一个prototype属性(Person.prototype),指向实例原型;
  • 每一个实例对象都有一个__proto__属性(person.__proto__),这个属性会指向该对象的原型;

Person.prototype === person.__proto__

  • 每个原型都有一个 constructor 属性指向关联的构造函数 原型指向构造函数;

Person.prototype.constructor === Person

function Person() {  

}

Person.prototype.name = 'Kevin';    

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

  • 创建构造函数 Person
  • 在原型 Person.prototype 上新增 name 属性,赋值为 Kevin
  • 通过 new 操作符新增一个继承自构造函数 Person 的实例对象 person
  • 在实例对象 person 新增一个 name 属性,赋值为 Daisy ,这一步我们称之为自定义属性
  • 输出实例对象person 上的 name 属性,会查找实例对象本身,优先找到自定义属性,所以值为 Daisy (所以自定义属性优先级高于原型上的自有属性,这也是为什么有了属性和方法的重写的概念)
  • 将实例对象 person 上的自定义属性 name 删除
  • 输出 person.name ,还是先查找实例对象本身,因为自定义属性被删除了,那么就去原型上面找,找到了之前定义在原型上的值,所以,输出 Kevin

原型链

原型链的概念呢,其实有点类似于作用域链,就是由于一层一层的嵌套,和链条一样,一节接一节,所以,原型产生的链条,称为原型链。

我们都知道对象的 __proto__ 就是所谓的原型,而原型又是一个对象,它又有自己的 __proto__,原型的 __proto__ 又是原型的原型,就这样可以一直通过 __proto__ 向上找,这就是原型链,当向上找找到 Object 的原型的时候,这条原型链就算到头了。

打印 person.__proto__.__proto__ ,原型链查找就算到头了,也就是再无 __proto__,一个简单的 person 实例对象,也有两层原型。

面向对象编程的相关知识点

Promise

Promise 本质上是一个构造函数,因为使用 new 关键字创建,它是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,属于 ES6 中的一个概念。

封装Http请求:

// 向后台发送数据
export const postData= (params) => {
    return new Promise((resolve, reject) => {
        axios.post('/xxxUrl', params).then(res => {
            resolve(res)
        })
        .catch(error => {
            reject(error)
        });
    })
};
  • Promise.prototype.then()

then 方法简单理解就是 resolve() 回调之后的产物,即 已成功 状态下的回调函数。

  • Promise.prototype.catch()

catch 方法简单理解就是 reject() 回调之后的产物,即 已失败 状态下的回调函数。

  • Promise.prototype.finally()

finally 方法简单理解就是 无论成功或失败 ,都会执行的回调函数。

链式调用

Promise 本来就是用来解决回调地狱所带来的问题的,所以,其实可以用链式调用的方式,来解决层层嵌套所带来的问题。

它的原理主要是 .then() 回调返回的也是一个 Promise ,并且是一个全新的 Promise,如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。

getData1().then(res=>{
  console.log(res)  // data1
  return getData2();
}).then(res=>{
  console.log(res) // data2
  return getData3();
}).then(res=>{
  console.log(res) // data3
})

异步解决方案

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,主要原因是它可以在执行中被中断、然后等待一段时间再被我们唤醒。

它内部有一个 yield 关键字,而它的主要作用,是将函数进行分割,你可以理解为将函数分为多个步骤,而每次函数的调用只执行一个步骤。这也就是上面所说的被中断,等下次继续唤醒

function* helloWorldGenerator() {
  yield 'result1';
  yield 'result2';
  return 'result3';
}

Generator 函数的声明和普通函数还是存在区别的,不过不仅如此,在函数的调用上也会有所不一样。在我们调用 Generator 函数之后,函数并不会执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,通过调用这个指针对象的 next() 方法就会从头开始执行,直到遇到第一个 yield 才会停止。等到下一次 next() 方法,又会从上一次停止的地方继续执行,再遇到下一个 yield 停止。

function getResult1(){
  return getData1().then(res=>{
    console.log('step1');
    // ....
  })
}

function getResult2(){
  return getData1().then(res=>{
    console.log('step2');
    // ....
  })
}

function getResult3(){
  return getData1().then(res=>{
    console.log('step3');
    // ....
  })
}

function* getGenerator(){
  console.log('start');
  yield getResult1();
  yield getResult2();
  yield getResult3();
}

let hw = getGenerator(); // 仅生成指针对象,并不执行
hw.next(); // 先输出 start ,然后输出 step1
hw.next(); // 输出 step2
hw.next(); // 输出 step3

完成整个 Generator 函数的执行。

async / await

async/awaitES7提出的一种异步解决方案,它相当于Generator + 执行器的语法糖,就目前来说,是最佳的异步解决方案,真正实现了异步代码,同步表示。

async function hw(){
  console.log('start');
  await getResult1();
  await getResult2();
  await getResult3();
}

回调函数

回调函数是JS异步编程的最基本、最原始的方式,例如事件回调、setTimeout/setInterval、ajax等

函数内部this的指向

函数内部this的指向

HTTP请求原理

  • 解析URL: 客户端解析所需访问的URL,通过DNS解析找到服务器的IP地址
  • 建立TCP连接: 客户端使用解析得到的服务器地址建立一个TCP连接,完成三次握手后,连接通道建立。若使用HTTPS协议,则还会进行SSL握手,建立加密通道。使用SSL握手时,会确定是否使用HTTP2。
  • 发送请求报文: 客户端构建HTTP请求报文,其中包括请求行、请求头和请求体。请求行包含请求方法(如GET、POST、PUT、DELETE等)、URL和HTTP版本。请求头包含与请求相关的附加信息,如用户代理(User-Agent)、Accept、Cookie等。请求体是可选的,通常用于POST请求发送表单数据或JSON数据。
  • 服务器响应: 服务器接收到请求后,会解析请求报文并进入后端流程。服务器执行相应的操作,并构建一个HTTP响应报文作为响应。
  • 接收响应报文: 服务器将构建好的HTTP响应报文发送回客户端。客户端接收到响应报文后,通过TCP连接接收数据。
  • 解析响应报文: 客户端解析响应报文,提取响应行、响应头和响应体。响应行包含响应状态码(如200表示成功、404表示未找到等)和HTTP版本。响应头包含与响应相关的附加信息,如响应内容的类型、响应长度等。响应体包含实际的响应数据,如HTML页面、JSON数据等。
  • 渲染页面或执行操作: 客户端根据响应的内容进行处理操作。对于HTML页面,客户端会解析并渲染页面,展示给用户。对于其他资源,如图片、CSS文件、JavaScript文件等,客户端可能会进一步发起请求以获取这些资源,并在页面中进行加载和使用。
  • 关闭TCP连接: 浏览器根据使用协议的版本,以及Connectionn字段的约定,决定是否要保留TCP连接。不保留,一旦HTTP请求和响应过程完成,客户端和服务器之间的TCP连接将会关闭。

浏览器渲染原理

  • 解析 HTML,构建DOM树:  
  • 样式计算:
  • 布局:
  • 分层:
  • 绘制:
  • 分块:
  • 格栅化:
  • 回流、重绘、合成: