• 前端面试中的常见问题


    URI和URL的区别?

    URI: Uniform Resource Identifier 指的是统一资源标识符

    URL: Uniform Resource Location 指的是统一资源定位符

    URN: Universal Resource Name 指的是统一资源名称

    URI 指的是统一资源标识符,用唯一的标识来确定一个资源,它是一种抽象的定义,也就是说, 不管使用什么方法来定义,只要能唯一的标识一个资源,就可以称为 URI。

    URL 指的是统一资源定位符,URN 指的是统一资源名称。URL 和 URN 是 URI 的子集,URL 可 以理解为使用地址来标识资源,URN 可以理解为使用名称来标识资源。

    模块化

    commonJS

    • nodeJS采用的是该规范。
    • 导出:module.exports = {}exports.xxx = 'xxx'
    • 导入:require(./index.js)
    • 特点:
      1. 所有代码在模块作用域内运行,不会污染其他文件
      2. require 得到的值是值的拷贝,即你引用其他 JS 文件的变量,修改操作了也不会影响其他文件
    • 缺陷:
      1. 同步加载问题。CommonJS 规范中模块是同步加载的,即在 index.js 中加载 index2.js,如果 index2.js 卡住了,那就要等很久。
      2. 应用层面。在 index.html 中做 var index = require('./index.js') 操作报错,因为它最终是后台执行的,只能是 index.js 引用 index2.js 这种方式。

    AMD

    • Asynchronous Module Definition 的缩写
    • requireJS是对amd的实现,是前端模块化管理的工具库,实现了js文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。

    CMD

    • CMD 则是依赖就近,用的时候再 require。
    • AMD和CMD最大的区别是对依赖模块的**执行时机的处理不同**,二者皆为异步加载模块;
    • AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。

    ES6 Module

    • es6原生支持的规范(目前需要使用babel来兼容)
    • 导入:
      1. import './index'
      2. import { a } from './index.js'
      3. import { a as jsliang } from './index.js'
      4. import * as index from './index.js'
    • 导出:
      1. export a
      2. export { a }
      3. export { a as jsliang }
      4. export default function() {}
    • 特点:
      1. export 命令和 import 命令可以出现在模块的任何位置,只要处于模块顶层就可以。 如果处于块级作用域内,就会报错,这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
      2. import 命令具有提升效果,会提升到整个模块的头部,首先执行。
    • 和 CommonJS 区别:
      1. CommonJS 模块是运行时加载,ES6 Modules 是编译时输出接口
      2. CommonJS 输出是值的拷贝;ES6 Modules 输出的是值的引用,被输出模块的内部的改变会影响引用的改变
      3. CommonJs 导入的模块路径可以是一个表达式,因为它使用的是require() 方法;而 ES6 Modules 只能是字符串
      4. CommonJS this 指向当前模块,ES6 Modules 的 this 指向 undefined
      5. ES6 Modules 中没有这些顶层变量:argumentsrequiremoduleexports__filename__dirname

    babel

    • Babel 是一个 JavaScript 编译器。他把最新版的javascript编译成当下可以执行的版本,简言之,利用babel就可以让我们在当前的项目中随意的使用这些新最新的es6,甚至es7的语法。

    原理

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHVvAzKl-1661952053815)(%E9%9D%A2%E8%AF%95%E5%87%86%E5%A4%87%2064d3ed4453ae44a38f71e8b99d75fcc4/Untitled%203.png)]

    1. 解析
      • 解析步骤接收代码并输出抽象语法树 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。
      • 词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。
      • 语法分析阶段会把一个令牌流转换成 AST 的形式。
    2. 转换
      • 转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。
      • Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
    3. 生成
      • 代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。
      • Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

    深拷贝和浅拷贝

    • 浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存。浅拷贝是拷贝一层,深层次的对象级别的就拷贝引用;

    • 深拷贝是拷贝多层,每一级别的数据都会拷贝出来;

    • 总结来看,浅拷贝的时候如果数据是基本数据类型,那么就如同直接赋值那种,会拷贝其本身,如果除了基本数据类型之外还有一层对象,那么对于浅拷贝而言就只能拷贝其引用,对象的改变会反应到拷贝对象上;浅拷贝就是拷贝了一层,除了对象是拷贝的引用类型,其他都是直接将值传递,有自己的内存空间的。

    • 但是深拷贝就会拷贝多层,即使是嵌套了对象,也会都拷贝出来。

    • 浅拷贝方式:

      1. for循环依次拷贝
      2. assign方法
        • var cloneObj1= Object.assign({}, obj1);
        • Object.assign 跟手动复制的效果相同,只能处理深度只有一层的对象。
    • 深拷贝方法

      1. 使用 Object.create()方法,var newObj = Object.create(oldObj)

      2. 转成 JSON 再转回来

        • var obj2 = JSON.parse(JSON.stringify(obj1));
        • console.log(obj1 === obj2);// false
        • console.log(obj1.body === obj2.body);// false
        • 它会抛弃对象的 constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object
        • 能正确处理的对象只有Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp 对象是无法通过这种方式深拷贝。function 没办法转成JSON。
        • var obj1 = { fun: function(){ console.log(123) } };
        • var obj2 = JSON.parse(JSON.stringify(obj1));
        • console.log(typeof obj1.fun);// 'function'
        • console.log(typeof obj2.fun);// 'undefined' <-- 没复制
      3. 递归拷贝

      var array = [
      	{ number: 1 }, { number: 2 }, { number: 3 }
      ];
      function copy (obj) {
      	var newobj = obj.constructor === Array ? [] : {}; 
      	if(typeof obj !== 'object'){
      		return; 
      	}
      	for(var i in obj){
      		newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i];
      	}
      	return newobj
      }
      var copyArray = copy(array)
      copyArray[0].number = 100;
      console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] 
      console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • 数组深拷贝

    1. slice()
      • var array = [1, 2, 3, 4];
        var copyArray = array.slice();
      • array.slice(start,end)方法返回一个从已有的数组中截取一部分元素片段组成的新数组(不改变原来的数组!)
    2. concat()
      • var copyArray = array.concat();
      • arrayObject.concat(arrayX,arrayX,......,arrayX)方法用于连接两个或多个数组,返回一个新的数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。例如arr1.concat(arr2,arr3),把arr1,arr2,arr3拼接起来。

    类数组对象arguments

    • arguments 是属于函数内部的变量,其值是函数参数列表,一个类数组对象,是具有长度属性的,却并不是数组。不具备slice()这个方法,那就意味着 arguments.slice()行不通。

    • var args = [].slice.call(arguments);

      通过[]创建一个空数组,通过slice方法从已有的数组中返回选定的元素。

      然后他通过call()方法强制指定当前函数方法内部变量arguments,返回了函数参数列表,这个时候slice()方法将其解析,因为没有给slice参数start和end,所以直接返回从0到最后一个位置的所有参数,将这些参数内部push到[]这个创建好的空数组中,这样子就返回了一个数组元素!

    css选择器优先级

    !important > 行内样式>ID选择器 > class选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性

    link@import的区别

    • link 是 XHTML 标签,除了加载 CSS 外,还可以定义 RSS 等其他事务;@import 属于 CSS 范畴,只能加载 CSS
    • link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载。
    • link 是 XHTML 标签,无兼容问题;@import 是在 CSS2.1 提出的,低版本的浏览器不支持。
    • link 支持使用 Javascript 控制 DOM 去改变样式;而 @import 不支持。

    算法从100万个数中取出前100个最大的数

    • 分块处理,然后再合并起来

      将100万分成100份,每份1万个数,取出每1万个数中前100个最大的数,再将这100份合并起来,取出前100个最大的数

    • 利用快排的思想,首先选择一个基数n[0],小的放前面,大的放后面,分成[a,b)b(b,d],递归,直到最右边的区间个数小于100个,不用管[a,b)。返回长度小于等于100的(b,d]区间,再返回一层一层去找的[a,b),取出剩下的,直到数量达到100.

    0.1+0.2≠0.3

    0.30000000000000004,JavaScript 遵循 IEEE 754 规范。

    在 JavaScript 中,所有的 Number 都是以 64-bit 的双精度浮点数存储的; 双精度的浮点数在这 64 位上划分为 3 段,而这 3 段也就确定了一个浮点数的值,64bit 的划分是“1-11-52”的模式,具体来说:

    1.就是 1 位最高位(最左边那一位)表示符号位,0 表示正,1 表示负;

    2.11 位表示指数部分;

    3.52 位表示尾数部分,也就是有效域部分

    由于只能存储52位尾数位,所以会出现精度缺失,把它存到内存中再取出来转换成十进制就不是原来的0.1了,就变成了0.100000000000000005551115123126,而为什么02+0.1是因为

    // 0.1 和 0.2 都转化成二进制后再进行运算 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111

    // 转成十进制正好是 0.30000000000000004

    • 解决方法:基本思路就是转成字符串,确定位数,先放大,运算后再缩小。
    /**
     ** 加法函数,用来得到精确的加法结果
     ** 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显。这个函数返回较为精确的加法结果。
     ** 调用:accAdd(arg1,arg2)
     ** 返回值:arg1加上arg2的精确结果
     **/
    function accAdd(arg1, arg2) {
      var r1, r2, m, c;
      try {
        r1 = arg1.toString().split(".")[1].length;
      } catch (e) {
        r1 = 0;
      }
      try {
        r2 = arg2.toString().split(".")[1].length;
      } catch (e) {
        r2 = 0;
      }
      c = Math.abs(r1 - r2);
      m = Math.pow(10, Math.max(r1, r2));
      if (c > 0) {
        var cm = Math.pow(10, c);
        if (r1 > r2) {
          arg1 = Number(arg1.toString().replace(".", ""));
          arg2 = Number(arg2.toString().replace(".", "")) * cm;
        } else {
          arg1 = Number(arg1.toString().replace(".", "")) * cm;
          arg2 = Number(arg2.toString().replace(".", ""));
        }
      } else {
        arg1 = Number(arg1.toString().replace(".", ""));
        arg2 = Number(arg2.toString().replace(".", ""));
      }
      return (arg1 + arg2) / m;
    }
    
    //给Number类型增加一个add方法,调用起来更加方便。
    Number.prototype.add = function(arg) {
      return accAdd(arg, this);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    0.2+0.3=0.5

    // 0.2 和 0.3 都转化为二进制后再进行计算 0.001100110011001100110011001100110011001100110011001101 + 0.0100110011001100110011001100110011001100110011001101 = 0.10000000000000000000000000000000000000000000000000001 //尾数为大于52位

    // 而实际取值只取52位尾数位,就变成了 0.1000000000000000000000000000000000000000000000000000 //0.5

    0.2 和0.3分别转换为二进制进行计算:在内存中,它们的尾数位都是等于52位的,而他们相加必定大于52位,而他们相加又恰巧前52位尾数都是0,截取后恰好是0.1000000000000000000000000000000000000000000000000000也就是0.5

  • 相关阅读:
    ncnn之三(补充):window环境下vs2022安装ncnn+protobuf
    RabbitMQ-交换机
    有2023最新的批量混剪软件的排名榜单吗?
    Golang 基础面试题 01
    【Asp.net】Asp.net core中IIS配置注意事项
    5G学习-OSI参考模型
    翻译生物医学论文,专业翻译公司的优势有哪些
    centos8.2 OS日志重定向到串口设备ttyS0,通过bmc查看调试
    基于Docker的ROS开发
    机器学习实战——训练模型
  • 原文地址:https://blog.csdn.net/m0_37247632/article/details/126632493