zoukankan      html  css  js  c++  java
  • 【面试篇】原生JS(上)

    互联网寒冬之际,各大公司都缩减了HC,甚至是采取了“裁员”措施,在这样的大环境之下,想要获得一份更好的工作,必然需要付出更多的努力。
    一年前,也许你搞清楚闭包,this,原型链,就能获得认可。但是现在,很显然是不行了。本文梳理出了一些面试中有一定难度的高频原生JS问题,部分知识点可能你之前从未关注过,或者看到了,却没有仔细研究,但是它们却非常重要。

    1. 基本类型有哪几种?null 是对象吗?基本数据类型和复杂数据类型存储有什么区别?

    • 1)js基本类型有6种:undefined,null,bool,number,string,symbol(ES6新增)
    • 2)虽然typeof null返回的值是object,但是null不是对象,而是基本数据类型的一种。
    • 3)基本数据类型存储在栈内存,存储的是
      复杂数据类型的值存储在堆内存,地址【指向堆中的值】存储在栈内存
      当我们把对象赋值给另外一个变量的时候,复制的是地址,它们指向同一块内存空间,当其中一个对象改变时,另一个对象也会改变。

    2. typeof 是否正确判断类型? instanceof呢? instanceof 的实现原理是什么?

    • typeof 能够正确的判断基本数据类型,但是除了null(typeof null 输出的是object)。
      typeof 无法准确判断复杂数据类型(typeof 一个函数可以输出‘function’,除此之外,输出的全是object,我们无法知道对象的类型)
    • instanceof 无法正确判断基本数据类型,但是可以准确判断复杂数据类型。
      instanceof 是通过原型链判断的,A instanceof B,在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null,即Object.proto.proto),仍然不等于B.prototype,那么返回false,否则返回true
      正确判断数据类型请戳:https://github.com/YvetteLau/Blog/blob/master/JS/data-type.js
      instanceof的实现代码:
       // L instanceof R
       function instance_of(L,R){ //L 为左表达式,R为右表达式
        var O = R.prototype; //取R的显示原型
        L = L.__proto__; //取L的隐式原型
        while(true){
            if(L === null) //已经找到顶层
                return false;
            if(O === L) //当O严格等于L时,返回true
                return true;
            L = L.__proto__; //继续向上一层原型链查找
        }   
       }
    

    3. for ,for of , for in ,$.each ,$().each 和 forEach,map 的区别。

    • for
      Javascript中的for循环,它用来遍历数组
    • for of
      ES6中新增加的语法 for of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。for of 允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等
      对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。
    //循环一个Map:
    let iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);    
    for (let [key, value] of iterable) {
      console.log(value);
    }
    // 1 2 3
    for (let entry of iterable) {
      console.log(entry);
    }
    //[a,1] [a,2] [a,3]
    //循环一个Set: ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
    let iterable = new Set([1, 1, 2, 2, 3, 3]);    
    for (let value of iterable) {
      console.log(value);
    }
    //1 2 3
    //循环一个拥有enumerable属性的对象
    //for of循环并不能直接使用在普通的对象上,但如果我们按对象所拥有的属性进行循环,可使用内置的Object.keys()方法:
    for (var key of Object.keys(someObject)) {
      console.log(key + ": " + someObject[key]);
    }
    

    原生js中的Object.keys方法详解:https://segmentfault.com/a/1190000015619348

    • for in
      遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
      for(var item in arr|obj){} 可以用于遍历数组和对象
    //遍历对象时,item表示key值,arr表示key值对应的value值 obj[item]
    var obj = {a:1,b:2,c:3};
    for(let item in obj){
    console.log('obj.' + item + '=' + obj[item]);
    }
    //遍历数组时,item表示索引值, arr表示当前索引值对应的元素 arr[item]
    var arr = ['a','b','c'];
    for (var item in arr) {
        console.log(arr[item]) //a b c
    }
    
    • forEach()
      只能遍历数组,不能continue跳过或者break终止循环(不能中断),没有返回值(或认为返回值是undefined)。
      forEach循环可以直接取到元素,同时也可以取到index值。
    let arr = ['a','b','c','d']
    arr.forEach(function(val,index,arr){
    //val是当前元素,index是当前元素索引,arr是数组
        console.log('index:'+ index + ',' + 'val:' + val)
    })
    
    • $.each(jQuery)
      $.each(arr|obj,function(key,val)){}
      可以用来遍历数组和对象,其中key表示索引值,val表示value值
    var arr = ['a','b','c']
    $.each(arr, function(key, val) {
        console.log(key, val);
    })
    //0 a  //1 b  //2 c
    
    • $().each()(jQuery)
      $().each()在dom处理上面用的较多,主要是用来遍历DOMList。如果页面有多个input标签类型为checkbox,对于这时用$().each()来处理多个checkbox
    $("input[name = 'checkbox']").each(function(i){
        if($(this).attr('checked') == true){
        //操作代码
        }
    })
    
    • forEach是否会改变原数组?
      除了forEach之外,map等API,也有同样的问题。
    //数组项是基本类型
    let array = [1, 2, 3, 4];
    array.forEach((item) => {
        item *= 10;
    });
    console.log(array); //[1, 2, 3, 4]
    array.forEach((item) => {
        array[1] = 10; //直接操作数组
    });
    console.log(array); //[ 1, 10, 3, 4 ]
    //数组项是复杂类型
    let array2 = [
        { name: "Yve" },
        { age: 20 }
    ];
    array2.forEach((item) => {
        item.name = 10;
    });
    console.log(array2);//[ { name: 10 }, { age: 20, name: 10 } ]
    

    4. 如何判断一个变量是不是数组

    • 1)使用Array.isArray(arr)判断,如果返回true,则是数组
    • 2)使用instanceof Array判断,如果返回true,则是数组
    • 3)使用Object.prototype.toString.call判断,如果值是[object Array],则是数组
    • 4)使用constructor来判断,如果arr.constructor === Array,则是数组(这种判断方式不准确,因为可以指定obj.constructor = Array)
    function fn() {
        console.log(Array.isArray(arguments));   //false; 因为arguments是类数组,但不是数组
        console.log(Array.isArray([1,2,3,4]));   //true
        console.log(arguments instanceof Array); //fasle
        console.log([1,2,3,4] instanceof Array); //true
        console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
        console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
        console.log(arguments.constructor === Array); //false
        arguments.constructor = Array;
        console.log(arguments.constructor === Array); //true
        console.log(Array.isArray(arguments));        //false
    }
    fn(1,2,3,4);
    

    5. 类数组与数组的区别

    类数组:
    1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)。元素按序保存在对象中,可以通过索引访问
    2)不具有数组所具有的方法
    3)类数组是普通对象,而真实数组是Array类型
    常见的类数组:函数的参数argumentsDOM对象列表(比如通过 document.querySelectorAll 得到的列表)、jQuery 对象 (比如 $("div"))

    //第一种方法:借用了数组原型中的slice方法(没有传入参数时,开始和结束索引为0和arr.length),返回一个数组。
    Array.prototype.slice.call(arrayLike, start);
    //第二种方法:扩展运算符(…)也可以将某些数据结构转为数组
    [...arrayLike];
    //第三种方法:Array.from()是ES6中新增的方法,可以将两类对象转为真正的数组:类数组对象和可遍历(iterable)对象(包括ES6新增的数据结构Set和Map)
    Array.from(arrayLike);
    

    6. == 和 === 有什么区别?

    • === 不需要进行类型转换,只有类型相同并且值相等时,才返回 true.
    • == 如果两者类型不同,首先需要进行类型转换。具体流程如下:
    1. 首先判断两者类型是否相同,如果相等,判断值是否相等.
    2. 如果类型不同,进行类型转换
    3. 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
    4. 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
      5.判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
    5. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断

    例题如下:

    let person1 = {
        age : 25;
    }
    let person2 = person1;
    person2.age = 20;
    console.log(person1 === person2);
    //true,复杂数据类型,比较的是引用地址
    

    思考:[] == ![] ?? true还是false
    1.首先,我们需要知道 ! 优先级是高于 == (更多运算符优先级可查看: 运算符优先级)
    2. []引用类型转换成布尔值都是true,因此 ![] 的是false
    3. 根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
    4. 根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,转成number就是这个数字,其它情况,均为NaN)
    5. 0 == 0; 为true

    7. ES6中的class和ES5的类有什么区别?

    https://segmentfault.com/a/1190000010654915

    1. ES6 class 内部所有定义的方法都是不可枚举的;
    2. ES6 class 必须使用 new 调用;
    3. ES6 class 不存在变量提升;
    4. ES6 class 默认即是严格模式;
    5. ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

    8. 数组的哪些API会改变原数组?

    修改原数组的API有:
    splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

    • array.splice(index,count,add)
      既可以删除特定的元素,也可以在特定位置增加元素,也可以删除增加同时搞定,index是起始位置,hm是要删除元素的个数,add是要增加的元素
    • array.reverse()
      把数组反向排序
    • array.fill(给定值,填充起始位置,填充截至位置)
      用给定值填充数组
    • array.copyWithin(target, start = 0, end = this.length)
      将指定位置的成员复制到其它位置(会覆盖原有成员),然后返回当前数组。
      target(必需):从该位置开始替换数据
      start(可选):从该位置开始读取数据,默认为0,如果是负值,表示倒数(右边第一位为-1)
      end(可选):到该位置前停止读取数据,默认为数组长度,如果是负值,表示倒数
    • array.sort()
      对数组进行排序,可接受参数,参数必须是函数,如果不没有参数 则是按照字符编码的顺序进行排序
    let arry = [10, 5, 40, 1000]
    console.log(arry.sort()) // [ 10, 1000, 40, 5 ]
    //如果数字想要按大小排列,可写入参数:
    let arr = [3,1,7];
    console.log(arr.sort((a,b) => a-b)) // [1,3,7]
    
    • array.push()
      把一个元素或多个元素增加到数组的末尾,返回值为新数组的长度array.length
    • array.pop()
      删除数组中最后一个元素,返回值为删除的元素
    • array.unshift()
      在数组的第一个元素前面添加一个元素或多个元素,返回值为新数组的长度array.length
    • array.shift()
      删除数组中第一个元素,返回值依然是被删除的元素
      不修改原数组的API有:
      slice/map/forEach/every/filter/reduce/entries/find/concat
    • array.slice(start,end)
      剪切数组,含头不含尾,返回剪切的数组
    • array.forEach(function(item,index))与array.map(function(item,index))
      两者都是对数组遍历,index表示数组索引,不是必须的参数
    • array.every(callback)
      用于检测数组中的所有元素是否满足指定条件,只有当数组中每一个元素都满足条件时,表达式返回true , 否则返回false
    • array.filter(callback)
      数组过滤,返回满足条件的元素组成的一个新数组
    let arr = [1,5,10,15];
    let arr1 = arr.filter(item => item > 5)
    console.log(arr1);
    
    • array.find(function(value,index,arr){...})
      1)find方法用于找出第一个符合条件的数组成员。它的参数是一个回调函数,数组中的每一个成员依次执行这个回调函数。
      2)如果找到第一个符合条件的成员,返回该成员。如果没有符合条件的,则返回undefined
      3)find方法的回调函数接受三个参数: value:当前值 | index:当前位置 | arr:原数组
    [1, 5, 10, 15].find(function(value, index, arr) {
      return value > 9;
    }) // 10
    

    注:查找成员位置 - arr.indexOf(ele)[如果不存在,则返回-1]

    9. let、const 以及 var 的区别是什么?

    • let和const定义的变量不会出现变量提升,而var定义的变量会提升
    • let和const定义了一个拥有块级作用域属性的变量
    • let和const不允许重复声明变量(会抛出错误)
    • let和const定义的变量在定义之前使用,会抛出错误(形成暂时性死区),而var不会
    • const声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
      js声明变量var、let、const详解:https://segmentfault.com/a/1190000015325807

    10. 在JS中什么是变量提升?什么是暂时性死区?

    • 变量提升就是变量在声明之前就可以使用,值为undefined。

    • 在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 不再是一个百分百安全的操作。

    typeof x; // ReferenceError(暂时性死区,抛错)
    let x;
    typeof y; // 值是undefined,不会报错
    var y;
    

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

    11. 如何正确的判断this? 箭头函数的this是什么?

    this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定

    • 1. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的实例对象【前提是构造函数中没有返回对象或者是function,否则this指向返回的对象/function】

    • 2. 函数是否通过call,apply调用,或者用了bind,如果是,那么this绑定的就是指定的对象

    • 3. 函数是否在某个上下文对象中调用(隐式绑定),如果是,那么this绑定的就是那个上下文对象。一般为obj.foo()。

    • 4. 如果以上都不是,那么使用默认绑定。在严格模式下绑定到undefined,否则绑定到全局对象

    • 5. 如果把null或undefined作为this绑定对象传入call、apply或bind,这些值在调用时会被忽略,实际应用的是默认绑定规则

    • 6. 箭头函数没有自己的this,它的this继承于上一层代码块的this

    12. 词法作用域和this的区别。

    • 词法作用域 -- 作用域是由书写代码时变量和函数声明的位置决定的
      通常来说,作用域一共有两种主要的工作模型:
    • 词法作用域
    • 动态作用域

    词法作用域是大多数编程语言所采用的模式,而动态作用域仍有一些编程语言在用,例如 Bash 脚本。

    而 JavaScript 就是采用的词法作用域,也就是在编程阶段,作用域就已经明确下来了。

    • this 机制跟动态作用域很相似,它是在调用时被绑定的,this 指向什么,完全取决于函数的调用位置

    13. 谈谈你对JS执行上下文栈和作用域链的理解。

    执行上下文是当前 JavaScript 代码被解析和执行时所在环境, JavaScript 中运行任何的代码都是在执行上下文中运行。
    执行上下文分类:

    • 1)全局执行上下文
    • 2)函数执行上下文

    执行上下文创建过程如下:

    • 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
    • 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
    • 确定this的值,即 ResolveThisBinding

    JS执行上下文栈是一个存储函数调用的栈结构,遵循先进后出的原则。

    • JavaScript执行在单线程上,所有的代码都是排队执行。
    • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
    • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
    • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
    • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

    作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。

    理解 JS 作用域链与执行上下文:
    https://juejin.im/post/5abf5b5af265da23a1420833

    14. 什么是闭包?闭包的作用是什么?闭包的使用场景

    闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

    闭包的作用有:

    • 1. 封装私有变量
    • 2. 模仿块级作用域(ES5中没有块级作用域)
    • 3. 实现JS的模块
  • 相关阅读:
    Head first javascript(七)
    Python Fundamental for Django
    Head first javascript(六)
    Head first javascript(五)
    Head first javascript(四)
    Head first javascript(三)
    Head first javascript(二)
    Head first javascript(一)
    Sicily 1090. Highways 解题报告
    Python GUI programming(tkinter)
  • 原文地址:https://www.cnblogs.com/sunidol/p/11301826.html
Copyright © 2011-2022 走看看