面试-js篇

65 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

bind、call、apply

this指向问题

bind call apply都可以改变this的指向问题

区别

bind和(call、apply)的区别

bind 返回一个改变了this指向的函数(永久改变)

call、apply直接改变this指向,并执行函数(这一次改变)

call 和 apply的区别

call 传参 :可以传入多个参数 第一个为this的指向,其余为函数所需的参数 apply 传参:只能传入两个参数 第一个为this的指向,第二个为数组,(数组中为函数所需的参数)

手写

    /手写call
Function.prototype.myCall=function(context=window,...args){
    //处理args,转成数组
    args = args ||[];
    //
    const key = Symbol();
    //绑定this 指向调用call的函数
    context[key] = this;
    const result = context[key](...args);
    //接触绑定
    delete context[key];

    return result 
}
// obj.say.myCall(tom,18,'nan');

//手写apply
Function.prototype.myApply = function(context=window,args){

    args = args ||[];
    //
    const key = Symbol();
    //绑定this 指向调用call的函数
    context[key] = this;
    const result = context[key](...args);
    //接触绑定
    delete context[key];

    return result 
        
}
// obj.say.myApply(tom,[19,'男'])

//手写bind
Function.prototype.myBind = function(){
    const fn = this;
    //第一个参数为改变的指向
    const context = arguments[0];
    // 获取剩余参数 类数组转数组对象
    const args = [].slice.call(arguments,1);
    return function(){
        // 获取参数
        const returnArgs = [].slice.call(arguments,1);

        fn.apply(context,returnArgs.concat(args))
    }
}

数组去重

    
//indexOf 和filter

function unique(arr){
    if(!Array.isArray(arr)){
        return console.error('is not a array');
    }
    let res = arr.filter((item,index)=>{
        //indexOf返回数组中第一个该元素的下标
        return arr.indexOf(item) == index;
    })
    return res;
}
console.log(unique(array));
//indexOf和循环

function unique(arr){
    if(!Array.isArray(arr)){
        return console.error('is not a array');
    }
    let res = [];
    for(let i =0;i<arr.length;i++){
        //使用用indexOf
        // if(res.indexOf(arr[i])===-1){
        //     res.push(arr[i])
        // }
        // 使用includes
        if(!res.includes(arr[i])){
            res.push(arr[i])
        }
    }
    return res;
}
console.log(unique(array));
function unique(arr){
    if(!Array.isArray(arr)){
        return console.log('not a array');
    }
    let res = [arr[0]];
    for(let i = 0;i<arr.length;i++){
        let flag = true
        for(let j = 0; j<res.length;j++){
            if(arr[i]===res[j]){
                flag = false;
                break
            }
        }
        if(flag){
            res.push(arr[i])
        }
    }
    return res
}
console.log(unique(array));
    // 相邻元素去重
function unique(arr){
    if(!Array.isArray(arr)){
        return console.log('not a array');
    }
    let res = [];
    //排序
    arr = arr.sort();
    for(let i = 0 ;i<arr.length;i++){
        if(arr[i]!==arr[i+1]){
            res.push(arr[i])
        }
    }
    return res
}
console.log(unique(array));
    //利用对象属性去重
function unique(arr){
    if(!Array.isArray(arr)){
        return console.log('is not a array');
    }
    let res = [];
    let obj = {};
    for(let i = 0;i<arr.length;i++){
        if(!obj[arr[i]]){
            res.push(arr[i]);
            obj[arr[i]] = 1;
        }
        else{
            obj[arr[i]]++;
        }
    }
    return res;
}

console.log(unique(array));
//ES6 set 去重

function unique(arr){
    if(!Array.isArray(arr)){
        return console.log('not a array');
    }
    //set 和解构赋值
    // return [...new Set(arr)]    
    //set 和Array.from
    return Array.from(new Set(arr))
}
function unique(arr){
    if(!Array.isArray(arr)){
        return console.log('not a array');
    }
   
    let obj = {}
    let res = arr.filter((item,index)=>{
        if(obj.hasOwnProperty(typeof item +item)){
            return false
        }
        else{
            obj[typeof item +item] = true;
            return true
        }
    })
    return res;
}
console.log(unique(array));

原型和原型链

所有的函数对象都有一个属性prototype,指向一个对象(原型对象),由该函数创建的实例的原型对象,原型对象的构造器指向函数

function Person(){
    this.name = 'jack';
    
}
let p = new Person();

实例对象的隐式原型对象(proto)指向构造函数的显示原型对象(prototype)

p.__proto__===Person.prototype

构造函数的原型对象的构造器(constructor)指向构造函数本身

Person.prototype.constructor === Person

所有对象都有__protp__属性

函数对象本质是由 new Function()创建,Function是所有函数对象的构造函数

Person.__proto__===Function.prototype

原型对象也是一个对象,也有__proto__,原型对象是一个普通对象,其构造函数时Object()

Person.prototype.__proto__ === Object.prototype

Object()是个对象,其构造函数是Function()

Object.__proto__===Function.prototype

Function()也是一个对象,有__proto__属性

Function.__proto__ === Function.prototype

Function的原型对象的隐式原型

Function.prototype.__proto__===Object.prototype

Object.protptype.__proto__===null

11.png

作用域和作用域链

静态作用域和动态作用域

一篇非常好的作用域和作用域链文章

静态作用域也叫词法作用域

作用域在声明的时候确定,而非在执行的时候确定

作用域本质上是一个对象AO。在作用域内,即是否为该作用域对象的属性

  var str = 'hello'
function say(){
    console.log(str);
}
function test(){
    var str = 'world'
    say();
}
test();//'hello'

动态作用域会打印world ,词法作用域打印hello

作用域链

当访问某个变量时,会现在当前AO中查找该属性,如果没有该属性,就向上去父级AO中查找属性,以此类推直到查找到根节点

执行上下文

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行

三种上下执行上下文

全局上下文

函数上下文

eval函数上下文

程序执行时会创建一个全局执行上下文,并且将其压入执行栈的底部(执行栈:先进后出的调用栈)当函数执行时会创建函数上下文并压入栈的顶部,当前函数执行完成就弹出。

执行上下文分两个阶段:创建阶段和执行阶段

创建阶段

this的绑定 创建词法环境(变量作用域)

一个函数可以访问在它的调用上下文中定义的变量,这个就是**词法作用域(Lexical scope)**

创建变量环境

执行阶段

闭包

当程序调用一个函数时,会发生什么?

学习参考文章

  1. JavaScript创建一个新的执行上下文,我们叫作本地执行上下文。
  2. 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  3. 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。 函数什么时候结束?当它遇到一个return语句或一个结束括号}。 当一个函数结束时,会发生以下情况:
  4. 这个本地执行上下文从执行堆栈中弹出。
  5. 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有return语句,则返回undefined
  6. 这个本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么 称为本地执行上下文中自有的变量。

闭包:我们终于找到了,丢失的那块。 它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。 任何函数具有闭包,甚至是在全局范围内创建的函数?答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。

//每一个函数都会有一个闭包。闭包会保存该函数读取的上层作用域的变量。
//let fn = function() {} fn保存了function 的函数声明和该函数的闭包
function outter(){
    let num =10;
    function inner(){
        console.log(++num);
    }
    return inner;
}
let test1 = outter();
test1 ();//11
test1 ();//12
let test2 = outter();
test2();//11

判断类型的方法

typeof

//基础类型
typeof 1 // "number" 
typeof 'a'  // "string"
typeof true  // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
//历史遗留问题null 返回 object
typeof null // object
//复杂类型 除去函数返回function 其余返回object


object. prototype .toString.call

返回"[object, 类型]",注意返回的格式及大小写,前面是小写,后面是首字母大写

    object.prototype.toString.call(1);//[object,number]
    object.prototype.toString.call(null);//[object,null]
    //内置对象也可以判断
    object.prototype.toString.call([1,2]);//[object,Array]
    object.prototype.toString.call(new Date);//[object,Date]
    
    

instanceof

防抖和节流

防抖:单位时间内只执行一次(应用场景:模糊搜索) 节流:间隔时间执行

    //手写防抖
export default function debounce(fn:Function,delay:number){
    clearTimeout(fn.id);
    //不能使用箭头函数
    return function(){
        let arg = arguments
        fn.id = setTimeout(()=>{
            fn.apply(this,arg);
        },delay)
    }
}
//手写节流
export default function throttle(fn:Function,delay:number){
   let timer:any=null;
   //不能使用箭头函数
    return function (){
        let args = arguments;
        if(timer){
            return ;
        }
        timer = setTimeout(()=>{
            fn.call(this,args);
            timer = null;
        },delay)
    }

}

数组拍平(扁平化)

学习 : ( js数组拍平(数组扁平化)的八种方式 )

使用Infinity作为flat的参数,使得无需知道被扁平化的数组的维度。

new

使用new关键字时,发生了什么

  1. 创建一个空的简单 JavaScript 对象(即 {} );
  2. 为步骤1新创建的对象添加属性 __proto__ ,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为 this 的上下文 ;
  4. 如果该函数没有返回对象,则返回 this 。
    //模拟new
function creatObject(Con) {
    //创建新对象
    let obj = {};

    //将obj的__proto__绑定构造函数的原型
    //不推荐使用 ob.__protp__ = Con.constructor;
    Object.setPrototypeOf(obj, Con.constructor);

    // 执行构造函数并接受返回值
    const res = Con.apply(obj, [].slice.call(arguments, 1));

    //若构造函数的返回值为对象,返回构造函数的返回值
    // 否则返回obj对象    
    return typeof res === 'object' ? res : obj
}

实现nstanceOf

js底层如何实现类型信息的存储

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息

typeof 根据机器码来判断数据类型,null的机器码全为0。对象的机器码 前三位为000;

这一问题导致 typeof null === 'object' //true

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

被要求实现instanceof,面试官到底在考我什么?

instanceof 的本质上是沿着实例对象的原型链查找,构造函数的原型是否在实例对象的原型链上

//实现 instanceof
// 实现instanceof本质上只要只要遍历实例对象的原型链,挨个往上查找看是否有与Fn的prototype相等的原型,直到最顶层Object还找不到,那么就返回false,否则结果就是true
function myInstanceof(obj,fnc){
    if(!(obj && ['object','function'].includes(typeof obj))){
        return false;
    }

    //获取obj的原型
    let proto = Object.getPrototypeOf(obj);

     //使用递归或者遍历
    //递归
    if(proto === fnc.prototype){
        return true;
    }
    else if(proto === null ){
        return false
    }
    else {
        return myInstanceof(proto,fnc)
    }

}