Js Ts 常见面试题

417 阅读15分钟

Js Ts 常见面试题

JavaScript

  1. 继承、闭包、原型、原型链

    继承:

    概念:通过某种方式,可以让某些对象访问到其他对象中的属性、方法,这种方式称之为继承。

    背景:有些对象会有方法,而这些方法都是函数(函数也是对象),如果把这些方法都放在构造函数中声明,则会产生内存浪费

    注意:js的继承都是建立在:方法在原型上创建、属性在实例上创建的前提下

    闭包:

    闭包是指有权访问另外一个函数作用域中的变量的函数

    闭包是指能够访问自由变量的函数。其中自由变量,是指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量

    原型:

    每个js对象(除null)创建的时候,都会关联另一个对象,这个对象就是原型,每一个对象都会从原型中“继承”属性

    原型链:

    实例对象与原型之间的连接,叫做原型链。proto(隐式连接)

    JS在创建对象的时候,都有一个叫做proto的内置属性,用于指向创建它的函数对象的原型对象protoType。

    内部原型(proto)和构造型的原型(protoType)

    1)每个对象都有一个proto属性,原型链上的对象正是依靠这个属性连接在一起

    2)作为一个对象,当你访问其中的一个属性或方法的时候,如果这个属性中没有这个方法 或者 属性,那么JavaScript引擎将会访问这个对象的proto属性所指向上一个对象,并在哪个对象中查找指定的方法或属性,如果不能找到,哪就会继续通过对象的proto属性指向的对象进行向上查找,直到链表结束

  2. 原生js有哪些缺点

    1)不能添加多个入口函数(window.onload),如果添加多个,后面会覆盖之前的

    2)原生Js代码冗余

    3)原生Js容错率低,前端代码错误后面代码无法执行

  3. 简述同步和异步的区别

    同步(Synchronous):一般是指在代码运行的过程中,由上到下逐步运行代码,每一部分运行完成后,下面的代码才能开始运行

    异步(Astnchronous):指的是当我们需要一些代码在执行的时候不会影响到其他代码的执行,也就是在执行代码的同时,可以进行其他的代码的执行,不用等待代码执行完成之后才执行后的代码。

  4. promise使用及实现、promise的并行执行和顺序执行

new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve()
            }, 1000)
            }).then(
            res => {},
            err => {}
            )
<!---->

    promise并行执行
    Promise.add([p1,p2]).then(res => {
        .....
    })

5. javascript的数据类型、数据检查、深浅拷贝

js数据类型: (基础类型)Number, String, Boolean, Undefined, Null, Symbol, Biglnt(es10新增)
(引用类型)ObjectArray, function, Date, RegExp

js数据检查: typeof

浅拷贝:将原对象或原数组得到引用直接赋给新对象,新数组。新对象、新数组只是原对象的一个引用 (Object.assign() / 扩展运算符... / 等)

深拷贝:创建一个新的对象和数组,将原对象的各项属性的值(数组的所有元素)拷贝过来,是“值”而不是“引用” (JSON.parse(JSON.stringify(XXXX)) 、 等)

6. js精度丢失是什么造成的

js中 number类型运算都需要先将十进制转二进制。但小数点后的位数转二进制会出现无限循环的问题,只能舍01,所以会出现小数点丢失问题。

math.js使用
math.add(a+b)//加
math.subtract(a-b)//减
math.multiply(a\*b)//乘
math.divide(a/b)//除

7. null和undefined的差异

总的来说 nullundefined 都代表空,主要区别在于 undefined 表示尚未初始化的变量的值,而 null 表示该变量有意缺少对象指向。

undefined
这个变量从根本上就没有定义
隐藏式 空值
null
这个值虽然定义了,但它并未指向任何内存中的对象
声明式 空值

8. 数组去重,map和set的区别

SetMap 主要的应用场景在于 数据重组 和 数据储存。 Set 是一种叫做 集合 的数据结构,Map 是一种叫做 字典 的数据结构。

集合(Set): ES6 新增的一种新的数据结构,类似于数组,成员唯一(内部元素没有重复的值)。且使用键对数据排序即顺序存储。

Set 本身是一种构造函数,用来生成 Set 数据结构。

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

# Map()是一组键值对的结构,用于解决以往不能用对象做为键的问题,具有极快的查找速度。(注:函数、对象、基本类型都可以作为键或值。)

    var m=new Map( );	//初始化一个空的 map
    m.set('Pluto',23);	//添加新的key-value 值
    m.has('Pluto');   //true	是否存在key ‘Pluto’
    m.get('Pluto');   	//23
    m.delete('Pluto');	//删除key   ‘Pluto ’

# set() SetMap类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Setvar s=new Set([1,2,3,3]);
    s.add(4);  // set{1,2,3,4}
    s.add(3); //set{1,2,3,4}
    s.size();   //4
    s.has(3);  //true

1.  Map是键值对,Set是值的集合,当然键和值可以是任何的值;
2.  Map可以通过get方法获取值,而set不能因为它只有值;
3.  都能通过迭代器进行for…of遍历;
4.  Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储
5.  map和set都是stl中的关联容器,map以键值对的形式存储,key=value组成pair,是一组映射关系。set只有值,可以认为只有一个数据,并且set中元素不可以重复且自动排序。

9. 暂时死区、变量提升、let, var, const 区别

暂时性死区(TDZ):

  function(){
  i = 2;
  let i;
  }

浏览器直接报错。

  暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。 常见面试题:

<script>
for(var i = 0 ;i < 10;i++){
    setTimeout(function(){
    console.log(i);
    },0);
} 
</script>

setimout 执行的时候 for 已经执行完毕,所以打印出来 10 个 9 应该改用let。

  1. 自执行函数

    在JavaScript中,会有自执行匿名函数:(function () {/code/} ) (),Self-executing anonymous function。

(function fun4(){
    console.log("fun4");
    }()); // "fun4"

11. ES6箭头函数this指向

(1)箭头函数中没有this : 这意味着 call() apply() bind() 无法修改箭头函数中的this
(2)箭头函数中的this指向 :访问上一个作用域的this
说人话:函数在哪个作用域声明,this就是谁 (本质是通过作用域链访问上一个作用域中的this)
(3)箭头函数与function函数this区别
function函数 : 谁调用我,我就指向谁,与声明无关
箭头函数 : 谁声明我,我就指向谁 ,与调用无关

12. call, apply, bind15、new实现

1newnew 的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递

    function Person(){
        this.name = 'Jack';
    }
    var p = new Person(); 
    console.log(p.name)  // Jack

·创建一个新对象
·将构造函数的作用域赋给新对象(this 指向新对象)
·执行构造函数中的代码(为这个新对象添加属性)
·返回新对象

2)apply & call & bind
call、apply 和 bind 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数
apply 和 call 的实现

    Function.prototype.call = function (context, ...args) {
        var context = context || window;
        context.fn = this;

        var result = eval('context.fn(...args)');
        delete context.fn

        return result;
    }

    Function.prototype.apply = function (context, args) {
        let context = context || window;
        context.fn = this;

        let result = eval('context.fn(...args)');
        delete context.fn

        return result;
    }

bind的实现

    Function.prototype.bind = function (context, ...args) {
        if (typeof this !== "function") {
        throw new Error("this must be a function");
        }

        var self = this;
        var fbound = function () {
        self.apply(
            this instanceof self 
                ? this 
                : context, 
            args.concat(Array.prototype.slice.call(arguments))
        );
        }

        if (this.prototype) { // 在特殊情况下.prototype会缺失
        fbound.prototype = Object.create(this.prototype);
        }

        return fbound;
    }

13. 垃圾回收和内存泄漏

# 垃圾回收

什么是垃圾?
一般来说没有被引用的对象就是垃圾,有几个例外如果几个对象引用形成一个环,互相引用,但别的数据访问不到,这几个对象也是垃圾,也要被清除
常用的垃圾回收策略是标记清除和引用计数,其中标记清除更为常用
垃圾回收会定期找出那些不继续使用的变量,然后释放其内存
垃圾回收器会按照固定的时间间隔周期性的执行
只有函数内的变量才可能被回收
function fn1() {
var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
var obj = {name:'hanzichi', age: 10};
return obj;
}
// 而当调用结束后,出了fn1的环境,那么该块内存会被自动释放
var a = fn1();
var b = fn2();

# 内存泄漏

不再用到的内存,没有及时释放,就叫做内存泄漏
1.循环引用
一个很简单的例子:一个DOM对象被一个Javascript对象引用
与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄露。
这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null
2.闭包
// 闭包可以维持函数内局部变量,使其得不到释放
在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)
3.DOM泄露
当原有的DOM被移除时,子结点引用没有被移除则无法回收
4.Times计时器泄露
定时器忘记关闭,一直就不会被清除

14. 数组方法、数组乱序, 数组扁平化

1)数组扁平化

    1. flat()
    - Array.prototype.flat()
    - 参数是:要拉平的层数
    - 参数是 Infinity 时表示不管嵌套多少层,都转成一元数组
    - flat() 不会改变原数组,该方法会返回一个新数组
    - 如果原数组有空位,flat()方法会跳过空位
    - 代码:
    const arr = [1, [2, 3, [4, 5, [6,7]]]] // [1,2,3,4,5,6,7]
    const res = arr.flat(Infinity) // 参数表示展开的层数,如果是Infinity表示不管多少层都转成一元数组
    console.log(res)


    2. flatMap()
    - flatMap()表示相对数组执行 map() 方法,在执行 flat()方法
    - 参数:第一个参数是一个遍历函数,函数的参数一次是 value index array
    - flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this
    - flatMap()也不会改变原数组,会返回一个新的数组
    - 注意:flatMap()默认之展开一层
    - 代码:
    const arr = [1, 2, 3]
    const res = arr.flatMap(i => [i, i*2]) // [1, 2, 2, 4, 3, 6]
    // 相当于:[[1, 2], [2, 4], [3, 6]]
    console.log(res)

    3. toString
    - 如果数组元素都是数值(注意所有的数组中的所有元素都要是数字),可以使用toString()
    - 然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。


    const arr = [1, [2, 3, [4, [5]]]]
    function flat(arr) {
        return arr.toString().split(',').map(item => +item)
        //  arr.toString()数组的字符串形式,会展平。      =>  1,2,3,4,5
        // arr.toString().split(',')以,为分隔符,将字符串转成数组  ["1", "2", "3", "4", "5"]
        // +item相当于Number(item)
    }
    const res = flat(arr)
    console.log(res)

    4. 使用reduce()
    var arr = [1, [2, [3, 4]]];
    function flatten(arr) {
        return arr.reduce(function(prev, next){
            return prev.concat(Array.isArray(next) ? flatten(next) : next)
        }, [])
    // 这里指定了初始值是 []
    // prev = []
    // next = 1,因为prev初始值是空数组,所以next的初始值是 1

    // reduce((accumulate, currentValue, index, arr) => {....}, [])
    // 第一个参数:是一个函数
        // 第一个参数:累积变量,默认是数组的第一个元素
        // 第二个参数:当前变量,默认是数组的第二个元素
        // 第三个参数:当前位置(当前变量在数组中的位置)
        // 第四个参数:原数组
    // 第二个参数:累积变量的初始值,注意如果指定了初始值,那么当前变量就从数组的第一个元素开始
    }

    console.log(flatten(arr))

    5. 使用 ...展开运算符

    let arr = [1, [2, [3, 4]]];
    function flat(arr) {
    while (arr.some(item => Array.isArray(item))) { // 循环判断是不是数组,是数组就展开
        arr = [].concat(...arr); // 每次都扁平一层
    }
    return arr;
    }
    console.log(flat(arr))

2)数组乱序

    方法1:
        arr.sort(() => Math.random() - .5)


        function a() {
        const arr = [1, 2, 3, 4, 5]
        arr.sort(() => Math.random() - .5)  // 因为Math.random的值区间是[0, 1) 所以 这里大于0和小于0的概率都是50%
        // sort自定义排序时接收一个函数作为参数
        // 函数返回值大于0,表示两个比较的成员,第一个排在第二个的后面
        // 函数返回值小于0,表示两个比较的成员,第一个排在第二个的前面
        // 函数返回值等于0,表示两个比较的成员,位置不变
        // 当小数是0点几时,可以省略前面的0
        console.log(arr)
        }


        -----------
        存在问题:
        1. 概率不均等
        2. 造成概率不均等的原因是,sort底层实现是用了插入排序
        3. 具体就是当无序部分插入到有序部分的元素,一旦找到位置,剩下的元素就没有在和缓存的值就行比较了
        4. 没有机会比较所有元素,所以得不到完全随机的结果
        如下:
        const times = [0, 0, 0, 0, 0, 0]
        for(i = 0; i < 100000; i++) {
            const arr = [1, 2, 3, 4, 5, 6]
            arr.sort(() => Math.random() - 0.5)
            times[arr[5] - 1] ++  
            // times[arr[5] - 1] ++ 表示arr数组最后一个位置上,各数字出现的次数
            // 当arr[5]是6时,times[5]位置的值+1,就是arr[5]是6的次数+1
        }
        console.log(times)
        // [19480, 5174, 15619, 14188, 18880, 26659] 概率分配不均,arr[5]是2的概率明显小了很多



        -----------
        sort排序源码:
        - 各个浏览器的sort()方法的实现方法不同
        - v8中
        - 当数组长度小于10时,用的是插入排序
        - 否则,使用的是插入排序和快速排序的混合排序



        -----------
        插入排序
        - 分类:
        - 直接插入排序:顺序发定位插入位置
        - 二分插入排序:二分法定位插入位置
        -   希尔排序  :缩小增量,多遍插入排序



        -----------
        直接插入排序:
        - 插入排序的思想:从无序数组中取出一个值,插入到有序数组中,插入后有序数组仍然有序(打牌)
        - 原理:
        1. 将数组分成两部分,左边是有序数组(最初只有一个元素),右边是无序数组(所以循环是从1开始,因为有序部分初始有一个元素)
        2. 从右边的无序部分依次取出一个值,插入到有序部分,直到取完无序部分
        3. 如果找有序部分的插入位置?:
            1. 先缓存需要插入的无序部分的值,用一个变量来缓存 let temp = arr[i]
            2. 从有序部分的最后位置找(arr[i - 1]),如果arr[i - 1] > arr[i] 则该元素往后移动一位
            3. 如果有序该位置仍然比需要插入的值大,有序中该位置的值,也后移动一位,直到 j>=0
        - 代码:
        // 直接插入排序
        const arr = [1, 4, 3, 2]
        const insert_sort = (arr) => {
            for(let i = 1, len = arr.length; i < len; i++) { // 循环数组的无序部分,从1开始,因为假设初始化时有序部分有一个元素
                let temp = arr[i] // 缓存需要插入有序部分的这个无序部分的值,因为有序部分可能会往后移动位置,将其覆盖
                let j = i - 1 // 有序部分的最后一个元素的位置,有序部分从最后的位置依次往前查找需要插入的位置
                while(j >= 0 && arr[j] > temp) { // 有序部分循环条件
                    arr[j+1] = arr[j] // 有序该位置值大于temp,则往后移动一位
                    j-- // 依次往前查找
                }
                arr[j+1] = temp // 循环完后,j+1就是需要插入的位置,因为条件是大于temp,不满足时,j+1就是要插入的位置
            }
            return arr // 最后返回数组
        }
        console.log(insert_sort(arr))

15. 防抖节流

# 防抖

(1)防抖Debounce情景
①有些场景事件触发的频率过高(mousemove onkeydown onkeyup onscroll)
②回调函数执行的频率过高也会有卡顿现象。 可以一段时间过后进行触发去除无用操作

(2)防抖原理
一定在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,以新的事件的时间为准,n 秒后才执行,等触发事件 n 秒内不再触发事件才执行。

# 节流

(1)节流Throttle情景
①图片懒加载
②ajax数据请求加载

(2)节流原理
如果持续触发事件,每隔一段时间只执行一次函数。
官方解释:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

节流防抖总结
防抖:原理是维护一个定时器,将很多个相同的操作合并成一个。规定在delay后触发函数,如果在此之前触发函数,则取消之前的计时重新计时,只有最后一次操作能被触发。

节流:原理是判断是否达到一定的时间来触发事件。某个时间段内只能触发一次函数。

区别:防抖只会在最后一次事件后执行触发函数,节流不管事件多么的频繁,都会保证在规定时间段内触发事件函数。

Typescript

  1. Ts常用类型

    1. string 字符串类型 export const str: string = "helloworld"; str.substr(3);

    2. number 数字类型 let num: number = 100; num++;

    3. boolean 布尔类型 const bool: boolean = true;

    4. 数组类型 const numArr: number[] = [1, 2, 3]; numArr.map((num) => ++num);

    5. 对象类型 type User = { name: string; age: number; isAdmin: boolean; }; const user: User = { name: "xiaoming", age: 18, isAdmin: false }; const { name, age, isAdmin } = user;

    6. 函数类型 type Fn = (n: number) => number; const fn: Fn = (num) => ++num; fn(1);

  2. 类型判断

    export function printId(id: string | number) {
        if (typeof id === 'string') {
            console.log(id.toUpperCase());
        } else {
            console.log(id);
        }
    }
    
    printId(101); // OK 
    printId('202'); // OK
    
  3. 类型断言 |

    export type Position = 'left' | 'right' | 'top' | 'bottom';
    const setPos = (pos: Position) => {
    //...
    };
    
    const handleChange = (value: string) => {
    setPos(value as Position);
    };
    
    handleChange('left');
    

持续更新....