zoukankan      html  css  js  c++  java
  • 前端JS面试

    JavaScript

    1、原始值和引用值类型和区别

    原始值类型:Number、String、Boolean、Null、Undefined

    引用值类型:Object、Array、Function、Date、RegExp

    区别:原始值存储在栈中,引用值把引用变量存储在栈中,而实际的对象存储在堆中,每一个引用变量都有一个指针指向其堆中的实际对象

    let a = 1;
    let b = a;
    a = 2;
    console.log(b); // 1
    

    原始变量赋值给另一个原始变量时,只是把栈中的内容复制给另一个原始变量,此时这两个原始变量互不影响

    let a = [1,2,3,4];
    let b = a;
    a.push(5);
    console.log(b); // [1,2,3,4,5]
    a = [6];
    console.log(b); // [1,2,3,4,5]
    

    引用变量赋值给另一个引用变量时,各自的变量名存储在栈中,而实际对象的值指向堆中同一个地址,当变量a通过方法改变值时,实际上只改变堆中的内容,但地址不变,因此b的值也会改变;但是当变量a通过非方法改变值时,系统会为a重新创建一个堆区,a的指针指向新的堆地址,而b的指针仍然指向旧的堆地址

    2、判断数据类型

    1.typeof

    typeof 对于原始数据类型除了 null 以外都能判断出来,但是对于引用数据类型,判断的结果都是 object

    console.log(typeof 1); // number
    console.log(typeof '1'); // string
    console.log(typeof true); // boolean
    console.log(typeof undefined); // undefined
    console.log(typeof null); // object
    console.log(typeof function () {}); // function
    console.log(typeof []); // object
    console.log(typeof {}); // object
    

    2.instanceof

    a instanceof b 判断 b 的原型对象是否在 a 的原型链上

    原理如下:

    function instanceOf(a, b) {
        let bp = b.prototype;
        let ap = a.__proto__;
        while(true){
            if(ap === bp){
                return true;
            }else if(ap === null){
                return false;
            }
            ap = ap.__proto__;
        }
    }
    

    instanceof 主要用于判断引用数据类型

    console.log([] instanceof Array); // true
    console.log([] instanceof Object); // true
    console.log(function(){} instanceof Function); // true
    console.log({} instanceof Object); // true
    console.log(function(){} instanceof Object); // true
    

    instanceof 也可以用来判断原始数据类型(null 和 undefined 除外)

    let a = 1;
    console.log(a instanceof Number); // false
    

    为什么返回 false 呢?因为 instanceof 是 Object instanceof constructor,如果不是 Object 都返回 false

    console.log(new Number(1) instanceof Number); // true
    console.log(typeof new Number(1)); // Object
    

    通过 new Number() 生成的实例就是 object

    3.Object.prototype.toString.call()

    利用 Object 原型对象上的 toString 方法可以精确的判断各种数据类型

    let test = Object.prototype.toString;
    console.log(test.call(1)); // [object Number]
    console.log(test.call('1')); // [object String]
    console.log(test.call(true)); // [object Boolean]
    console.log(test.call(null)); // [object Null]
    console.log(test.call(undefined)); // [object Undefined]
    console.log(test.call([])); // [object Array]
    console.log(test.call({})); // [object Object]
    

    4.constructor

    通过实例对象原型上的 constructor 属性来判断数据类型(null 和 undefined 除外)

    console.log(1.constructor === Number); // true
    console.log(true.constructor === Boolean); // true
    console.log("1".constructor === String); // true
    console.log([].constructor === Array); // true
    console.log(function(){}.constructor === Function); // true
    console.log({}.constructor === Object); // true
    

    3、类数组与数组的区别与转换

    类数组

    1. 拥有 length 属性,其他属性(索引)为非负整数(对象中的索引会被当做字符串来处理)
    2. 不具有数组的方法
    3. 类数组是一个普通对象,而数组是 Array 类型

    常见的类数组有:

    1. 函数的参数 arguments
    2. DOM 方法返回的结果
    3. jQuery 对象(比如 $('div'))

    类数组转换为数组

    1. Array.prototype.slice.call

      const divs = document.querySelectorAll('div');
      const newDivs = Array.prototype.slice.call(divs);
      
    2. 扩展运算符

      const divs = document.querySelectorAll('div');
      const newDivs = [...divs];
      
    3. Array.from

      const divs = document.querySelectorAll('div');
      const newDivs = Array.from(divs);
      

    4、数组的常见API

    isArray():判断是否为数组

    const arr = [1, 2, 3];
    console.log(Array.isArray(arr)); // true
    

    toString():将数组转换为以逗号分隔的字符串

    const arr = [1, 2, 3];
    console.log(arr.toString()); // 1,2,3
    

    join():返回按照指定字符分隔的字符串

    const arr = [1, 2, 3];
    console.log(arr.join('-')); //1-2-3
    

    concat():用于连接两个或多个数组,该方法不会改变原数组,而仅仅会返回被连接数组的一个副本

    const arr = [1, 2, 3];
    const arr1 = [4, 5, 6];
    console.log(arr.concat(arr1)); // [1,2,3,4,5,6]
    

    slice(start, end):截取数组中的元素,该方法不会改变原数组,而是返回一个子数组

    start 和 end均为空时,截取数组所有元素

    const arr = [1, 2, 3];
    console.log(arr.slice()); // [1,2,3]
    

    end 为空时,从 start 开始截取到数组结尾

    const arr = [1, 2, 3];
    console.log(arr.slice(1)); // [2,3]
    

    end 不为空时,从 start 开始截取到 end 的前一位

    const arr = [1, 2, 3];
    console.log(arr.slice(1, 2)); // [2]
    

    start 和 end 为负数时,从数组末尾开始计算

    const arr = [1, 2, 3];
    console.log(arr.slice(-3, -1)); // [1,2]
    

    reverse():翻转数组

    const arr = [1, 2, 3];
    console.log(arr.reverse()); // [3,2,1]
    

    sort():自定义排序

    const arr = [1, 2, 3];
    console.log(arr.sort(function (a, b) {
        //return a - b; 从小到大排序
        return b - a; // 从大到小排序
    }));
    

    splice():向数组中添加/删除元素,该方法会改变原数组

    参数 描述
    index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
    howmany 必需。要删除的项目数量。如果设置为 0,则不会删除项目。
    item1, ..., itemX 可选。向数组添加的新项目。

    添加元素

    const arr = [1, 2, 3];
    arr.splice(3,0,4,5);
    console.log(arr); // [1,2,3,4,5]
    

    删除元素

    const arr = [1, 2, 3];
    arr.splice(2,1);
    console.log(arr); // [1,2]
    

    替换元素

    const arr = [1, 2, 3];
    arr.splice(2,1,4);
    console.log(arr); // [1,2,4]
    

    push():向数组末尾添加一个或多个元素,并返回新的长度

    const arr = [1, 2, 3];
    console.log(arr.push(4)); // 4
    console.log(arr); // [1,2,3,4]
    

    unshift():向数组开头添加一个或多个元素,并返回新的长度

    const arr = [1, 2, 3];
    console.log(arr.unshift(0)); // 4
    console.log(arr); // [0,1,2,3]
    

    pop():删除数组末尾的元素

    const arr = [1, 2, 3];
    arr.pop();
    console.log(arr); // [1,2]
    

    shift():删除数组开头的元素

    const arr = [1, 2, 3];
    arr.shift();
    console.log(arr); // [2,3]
    

    entries():返回数组的可迭代对象

    const arr = ['张三', '赵四', '王五'];
    let iterator = arr.entries();
    for(let v of iterator){
        console.log(v);
    }
    /*
    	[0, "张三"]
    	[1, "赵四"]
    	[2, "王五"]
    */
    

    every():检测数组的每个元素是否都符合条件,不会对空数组进行检测,不会改变原数组

    const arr = [1, 2, 3];
    const result = arr.every(item => item > 0   );
    console.log(result); // true
    

    fill():用一个固定值来填充数组

    const arr = [1, 2, 3];
    arr.fill(6);
    console.log(arr); // [6,6,6]
    

    filter():检测数组元素,并返回符合条件的所有元素的数组,不会对空数组进行检测,不会改变原数组

    const arr = [1, 2, 3];
    console.log(arr.filter(item => item>1)); // [2,3]
    

    find():返回数组中符合条件的第一个元素,空数组不会执行,不会改变原数组

    const arr = [1, 2, 3];
    console.log(arr.find(item => item>1)); // 2
    

    findIndex():返回数组中符合条件的第一个元素的索引,空数组不会执行,不会改变原数组,如果不存在,则返回 -1

    const arr = [1, 2, 3];
    console.log(arr.findIndex(item => item > 1)); // 1
    console.log(arr.findIndex(item => item > 4)); // -1
    

    forEach():遍历数组

    const arr = [1, 2, 3];
    arr.forEach(item => {
        console.log(item);
    })
    

    from():从一个类数组或可迭代对象创建一个新的浅拷贝的数组

    console.log(Array.from('foo'));
    // ["f", "o", "o"]
    
    console.log(Array.from([1, 2, 3], x => x + x));
    // [2,4,6]
    

    flat():按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

    扁平化嵌套数组

    const arr1 = [1, 2, [3, 4]];
    arr1.flat(); 
    // [1, 2, 3, 4]
    
    const arr2 = [1, 2, [3, 4, [5, 6]]];
    arr2.flat();
    // [1, 2, 3, 4, [5, 6]]
    
    const arr3 = [1, 2, [3, 4, [5, 6]]];
    arr3.flat(2);
    // [1, 2, 3, 4, 5, 6]
    
    //使用 Infinity,可展开任意深度的嵌套数组
    const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
    arr4.flat(Infinity);
    // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    

    扁平化并移除数组空项

    const arr4 = [1, 2, , 4, 5];
    arr4.flat();
    // [1, 2, 4, 5]
    

    includes():用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false

    const arr = [1,2,3];
    console.log(arr.includes(1)); // true
    

    indexOf():返回数组中给定元素的第一个索引,如果不存在,则返回-1

    const arr = [1,2,3,1];
    console.log(arr.indexOf(1)); // 0
    console.log(arr.indexOf(1,2)); // 3
    console.log(arr.indexOf(4)); // -1
    

    reduce():对数组中的每个元素执行一个提供的函数(升序执行),并将结果汇总为单个返回值

    const arr = [1,2,3,4,5,6,7,8,9,10];
    // 累加必须提供初始值
    console.log(arr.reduce((acc, cur) => acc + cur, 0)); // 55
    

    map():创建一个新数组,其结果是该数组中的每个元素调用提供的函数后的返回值

    const arr = [1,2,3];
    console.log(arr.map(item => item*2)); 
    // [2,4,6]
    

    考点:

    通常情况下,map 方法中的 callback 函数只需要接受一个参数,就是正在被遍历的数组元素本身。但这并不意味着 map 只给 callback 传了一个参数。例如:

    const arr = ['1','2','3'];
    console.log(arr.map(parseInt));
    

    期望输出 [1,2,3],然而实际结果是 [1,NaN,NaN]

    parseInt 经常被带着一个参数使用, 但是这里接受两个。第一个参数是一个表达式,第二个是callback function的基,Array.prototype.map 传递3个参数:

    • the currentValue
    • the index
    • the array

    第三个参数被parseInt忽视了, 但第二个参数会被使用

    参数 描述
    string 必需。要被解析的字符串。
    radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

    上述的迭代步骤为:

    // parseInt(string, radix) -> map(parseInt(value, index))
    /* 1 */ parseInt('1',0); // 1
    /* 2 */ parseInt('2',1); // NaN 
    /* 3 */ parseInt('3',2); // NaN 二进制只有0和1
    

    解决方案:

    function returnInt(element) {
      return parseInt(element, 10);
    }
    
    ['1', '2', '3'].map(returnInt); // [1, 2, 3]
    // 指定基数即进制为10
    
    // 只给parseInt传入当前元素值
    ['1', '2', '3'].map( item => parseInt(item) );
    
    ['1', '2', '3'].map(Number); // [1, 2, 3]
    

    参考 MDN Array

    5、bind、call、apply的区别

    bind():该方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值

    语法:

    var fn = Function.bind(obj, [param1[,param2][,...paramN]])
    

    使用场景为函数不需要立即调用,但又想改变函数内部的 this 指向(比如定时器内部的 this)

    const btn = document.querySelector('button');
    btn.onclick = function () {
        this.disabled = true;
        setTimeout(function () {
            this.disabled  =false;
        }.bind(this), 2000);
    }
    

    bind() 主要是为了改变函数内部的 this 指向

    apply():apply() 方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组(参数数组可以是数组实例,也可以是 arguments 对象)

    语法:

    Function.apply(obj, args)
    // args将作为参数传递给Function
    

    使用场景主要与数组有关

    1.Math.max 实现得到数组的最大项

    const arr = [1,2,3];
    console.log(Math.max.apply(Math, arr)); // 也可以使用null,但严格模式下还是要使用Math
    // 3
    

    2.Array.prototype.push 实现合并两个数组

    const arr1 = [1,2,3];
    const arr2 = [4,5,6];
    Array.prototype.push.apply(arr1, arr2);
    console.log(arr1);
    // [1,2,3,4,5,6]
    

    call():call() 方法与 apply() 方法类似,接收参数的方式有些不同,第一个参数为在其中运行函数的作用域,其余参数都直接传递给函数,即传递给函数的参数必须逐个列举出来

    语法:

    Function.call(obj, [param1[,param2][,...paramN]])
    // param参数列表会直接传递给Function
    

    使用场景是可以实现继承

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    function Student(name, age, id) {
        Person.call(this, name, age);
        this.id = id;
    }
    let s = new Student('张三', 20, '007');
    console.log(s);
    // Student {name: "张三", age: 20, id: "007"}
    

    6、new 的原理

    1. 在内存中创建一个空对象
    2. 给这个空对象添加属性和方法
    3. 将构造函数的 this 指定为创建的新对象,并将参数传入
    4. 如果构造函数没有手动返回对象,则返回第一步创建的对象,如果构造函数有返回对象,则 this 指向构造函数返回的对象

    实现原理如下:

    function Person(name) {
        this.name = name;
    }
    function newObject(parent, ...args) {
        let child = {};
        child.__proto__ = parent.prototype;
        parent.apply(child, args);
        return child;
    }
    const p = newObject(Person, '张三');
    console.log(p);// 张三
    Person.prototype.sayName = function () {
        console.log('我叫'+this.name);
    };
    p.sayName(); // 我叫张三
    

    7、如何正确判断 this 的指向

    this:谁调用它,this 就指向谁

    1.普通函数:this 指向 window,严格模式下('use strict')会抛出错误 undefined

    // 'use strict';
    var name = '张三';
    function fn() {
        console.log(this.name);
    }
    fn();
    

    2.对象函数:this 指向该函数所属对象

    var obj = {
        sayHello(){
            console.log(this);
        }
    }
    obj.sayHello();
    // {sayHello: f}
    

    3.构造函数:如果构造函数没有返回对象,则 this 指向创建的对象实例;如果构造函数有返回对象,则 this 指向返回的对象

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    var p = new Person('张三', 20);
    console.log(p);
    // Person {name: "张三", age: 20}
    
    function Person(name, age) {
        this.name = name;
        this.age = age;
        let obj = {
            name: '赵四',
            age: 18
        };
        return obj;
    }
    var p = new Person('张三', 20);
    console.log(p);
    // {name: '赵四', age: 18}
    

    4.绑定事件函数:this 指向事件的调用者

    var btn = document.querySelector('button');
    btn.onclick = function () {
        console.log(this);
    }
    // <button>点击</button>
    

    5.定时器函数:this 指向 window

    setTimeout(function () {
        console.log(this);
    },1000);
    

    6.立即执行函数:this 指向 window

    (function() {
        console.log(this);
    })();
    

    7.箭头函数:不绑定 this,this 指向函数定义位置的上下文

    btn.onclick = function () {
        setTimeout(() => {
            console.log(this);
            // <button>点击</button>
        },1000)
    }
    // 通过箭头函数可以改变定时器函数的this指向
    

    8.显式绑定:函数通过 call()、apply()、bind()方法绑定,this 指向方法中传入的对象

    function fn() {
        console.log(this);
    }
    var person = {
        name: '张三'
    };
    fn.call(person);
    fn.apply(person);
    fn.bind(person)();
    // {name: '张三'}
    

    如果这些方法中传入的第一个参数是 undefined 或 null,严格模式下 this 指向传入的值 undefined 或 null;非严格模式下 this 指向 window

    function fn() {
        console.log(this);
    }
    fn.call(null);// window
    
    'use strict';
    function fn() {
        console.log(this);
    }
    fn.call(null); // null
    

    9.隐式绑定:函数的调用时在某个对象上触发的,即调用位置存在上下文对象(相当于对象函数中的 this 指向)典型的隐式绑定为 xxx.fn()

    function fn() {
        console.log(this.name);
    }
    var person = {
        name: '张三',
        fn
    };
    person.fn(); // 张三
    

    8、变量提升与函数提升

    变量提升:将变量的声明提升到它所在作用域的顶端去执行,将赋值放在代码所在的位置(注意只有 var

    才存在变量提升)

    console.log(a);
    var a = 1; // undefined
    

    上述代码的实际执行顺序如下:

    var a;
    console.log(a);
    a = 1;
    

    而如果先进行赋值:

    a = 1;
    var a;
    console.log(a); // 1
    

    声明提升到顶端,所以输出1

    console.log('1-'+v1);
    var v1 = 100;
    function foo() {
        console.log('2-'+v1);
        var v1 = 200;
        console.log('3-'+v1);
    }
    foo();
    console.log('4-'+v1);
    // 1-undefined
    // 2-undefined
    // 200
    // 100
    

    函数提升:函数提升是整个代码块提升到它所在作用域的顶端执行

    console.log(fn);
    function fn () {
        console.log(1);
    }
    /*
    ƒ fn () {
            console.log(1);
        }
    */
    

    执行顺序相当于:

    function fn () {
        console.log(1);
    }
    console.log(fn);
    

    函数提升存在函数优先原则:

    foo(); //1
    var foo;
    function foo () {
        console.log(1);
    }
    foo = function () {
        console.log(2);
    }
    

    9、作用域与作用域链、执行上下文

    作用域:变量在某个范围内生效,目的是为了提高程序的安全性,减少命名冲突,分为全局作用域和局部作用域

    • 全局作用域:script 标签,或者整个 js 文件
      • 全局变量:
        1. 在全局作用域下的变量
        2. 在函数内部没用声明,直接赋值
        3. 只有在浏览器关闭时才会销毁,消耗内存
    • 局部作用域:函数内部,变量只在函数内部生效
      • 局部变量:
        1. 在局部作用域下的变量
        2. 函数的形参可以看做局部变量
        3. 当程序执行完毕就会销毁,节约内存资源

    作用域链:一般情况下,变量的取值是到创建该变量的函数作用域下查找,但是如果在当前作用域下没有查找到,就会向上一级作用域查找,直到全局作用域,这样一个查找过程形成的链称为作用域链。作用域链相当于内部函数访问外部函数的变量,采取的是链式查找的方式来决定取哪个值。

    //第一种情况,当函数作为参数
    var x = 10;
    function show(callback) {
        var x = 5;
        callback && callback();
    }
    function fun() {
        console.log(x);// 10 函数fun的上级作用域是全局作用域
    }
    show(fun);
    //第二种情况,当函数作为返回值输出
    var x = 10;
    function show() {
        var x = 5;
        return function() {
            console.log(x);// 5 函数的上级作用域是show函数
        }
    }
    var res = show();
    res();
    

    执行上下文:当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被提升,有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文,JavaScript 运行任何代码都是在执行上下文中运行

    执行上下文分类:

    • 全局执行上下文:不在任何函数中的代码都位于全局执行上下文中,代码首先进入的环境
    • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文,相当于该函数被调用时执行的环境
    • eval 函数执行上下文:运行在 eval 函数中的代码也有自己的函数执行上下文(不常用)

    执行上下文栈:也叫调用栈,执行上下文栈用于存储代码运行期间创建的所有执行上下文,JS 代码首次运行,先创建一个全局执行上下文并压入栈中,之后每次函数调用,都会创建一个函数执行上下文并压入栈中,当函数调用完成后,这个函数执行上下文以及其中的数据都会被销毁,然后重新进入全局执行上下文

    var a = 10 ;                     // 1.进入全局上下文环境
    var fun;
    var bar = function (x) {
        var b = 10;
        fun(x + b);              // 3.进入fun上下文环境
    }
    fun = function (y) {
        var c = 20;
        console.log(y + c);
    }
    bar(5);                          // 2.进入bar上下文环境
    

    执行上下文的生命周期:

    1. 创建阶段
      1. 创建变量:首先初始化函数的参数 arguments,提升函数声明和变量声明(预解析)
      2. 创建作用域链:作用域链本身包含变量,作用域链用于解析变量,当被要求解析变量时, JS 始终从代码嵌套的最内层开始查找,如果最内层没有找到,则会向上一级作用域查找,直到找到该变量
      3. 确定 this 的指向
    2. 执行阶段:变量赋值,代码执行
    3. 回收阶段:执行上下文出栈等待虚拟机回收执行上下文

    执行上下文特点:

    1. 单线程,在主进程上运行
    2. 同步执行,从上往下按顺序执行
    3. 全局执行上下文只有一个,浏览器关闭时会被弹出栈(销毁)
    4. 函数执行上下文没有数目限制
    5. 函数每被调用一次,都会创建一个新的执行上下文环境
    6. 函数调用完毕时,函数执行上下文以及其中的数据都会被销毁

    10、闭包及其作用

    高阶函数

    高阶函数时对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值;JavaScript 的回调函数是以实参形式传入其他函数中,也属于高阶函数

    // 1.将函数作为参数(回调函数)
    function fn(callback) {
        callback && callback();
    }
    fn(function () {
        console.log('hello');
    })
    
    // 2.将函数作为返回值
    function fun() {
        return function () {
            console.log('world');
        }
    }
    fun()();
    

    变量作用域

    1. 函数内部不可以访问全局变量
    2. 函数外部不可以访问局部变量
    3. 当函数执行完毕,本作用域内的局部变量会被销毁

    闭包

    闭包指有权访问另一个函数作用域中的变量的函数,闭包允许函数访问局部作用域之外的数据,即使外部函数已经退出,外部函数中的变量仍然可以被内部函数访问到,闭包的主要作用:延伸了变量的作用范围

    闭包实现的三个条件:

    1. 内部函数访问外部函数的变量
    2. 外部函数已经退出
    3. 内部函数仍然可以访问
    function fn() {
        var a = 1;
        return function (b) {
            a = a + b;
            console.log(a);
        }
    }
    var f = fn();
    f(1); // 2
    f(2); // 4
    

    上述函数执行的时候,f 得到的是闭包对象的引用,fn 函数执行完毕退出,但是 fn 函数中的活动对象由于闭包的存在并没有被销毁,执行 f 函数仍然可以访问到 a 变量,而执行 f(2)后 a 变量的值为4,因为闭包的引用,f 并没有消除

    闭包的核心内容:有些情况下(函数调用返回一个函数),函数调用完成之后,其执行上下文环境不会被销毁,所以使用闭包会增加内存开销,在 IE 中可能导致内存泄露,解决方法:在退出函数之前,将不使用的局部变量全部清除(变量赋值为null)

    11、原型和原型链

    原型:在 JavaScript 中,每一个函数都有一个 prototype 对象属性,指向另一个对象(原型对象),prototype 的所有属性和方法都会被构造函数的实例所继承。所以,我们可以把那些公共不变的方法,直接定义在 prototype 对象属性上 (一般情况下,公共属性定义在构造函数里,公共方法定义在原型对象上)

    原型链:JavaScript 成员查找机制是按照原型链来查找的(就近原则)

    1. 当访问一个对象的属性(或方法)时,首先查找对象是否拥有该属性(或方法)
    2. 如果没有,就找它的原型(__proto__)指向的构造函数的原型对象(prototype)
    3. 如果还没有,就找原型对象的原型指向的 Object 的原型对象
    4. 以此类推,递归访问 __proto__,直到找到,找不到则为 null

    12、prototype 与 __proto__ 的关系与区别

    prototype(显式原型属性):只有函数对象才具有 prototype 属性,这个属性指向一个对象,这个对象包含所有实例共享的属性和方法,这个对象也有一个属性 constructor,指回原构造函数

    __proto__(隐式原型属性):所有对象都具有该属性,指向构造该对象的构造函数的原型

    13、继承的实现方式及比较

    父类:

    function Parent(name) {
        this.name = name;
        this.arr = [1,2,3];
    }
    Parent.prototype.showName = function () {
        console.log(this.name);
    }
    

    1.原型继承:子类构造函数的原型等于父类构造函数的实例

    function Child() {}
    Child.prototype = new Parent();
    var c = new Child();
    console.log(c);
    

    优点:实例可以继承构造函数的属性,父类构造函数的属性,父类构造函数原型的属性

    缺点:

    1. 实例无法向父类构造函数传参

    2. 所有实例都会共享父类的引用类型属性

      var c = new Child();
      var c1 = new Child();
      c.age = 20;
      c.arr.push(4);
      console.log(c1.arr); // [1,2,3,4]
      

    2.借用构造函数:利用 call() 方法将父类构造函数引入子类构造函数

    function Child(name, age) {
        Parent.call(this, name);
        this.age = age;
    }
    var c = new Child('张三', 20);
    console.log(c);
    

    优点:

    1. 实例可以向父类构造函数传参
    2. 父类原型属性不会共享

    缺点:

    1. 只能继承父类构造函数的属性,不能继承原型属性
    2. 无法实现构造函数的复用(每次实例化都会重新调用)

    3.组合继承:将原型继承和借用构造函数继承组合(常用)

    function Child(name, age) {
        Parent.call(this, name);
        this.age = age;
    }
    Child.prototype = new Parent();
    var c = new Child('张三', 20);
    console.log(c);
    c.showName();
    

    优点:

    1. 可以继承父类原型上的属性,实例可以向父类构造函数传参,构造函数可以复用
    2. 每个实例引入的构造函数属性是私有的

    缺点:

    1. 调用了两次父类构造函数(消耗内存)
    2. 子类构造函数会代替原型上的父类构造函数

    4.原型式继承:将一个函数的原型指向父类实例,然后返回这个函数的对象

    function Child(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
    }
    var p = new Parent('张三');
    var c = new Child(p);
    console.log(c);
    

    缺点:

    1. 所有实例都会共享父类的引用类型属性
    2. 子类实例化对象时无法传参

    5.寄生式继承:给原型式继承再嵌套一层,实现传参

    function Child(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
    }
    var p = new Parent('张三');
    // 以上是原型式继承
    function ChildObject(obj, age) {
        var c = Child(obj);
        c.age = age;
        return c;
    }
    var c2 = new ChildObject(p, 20);
    console.log(c2);
    

    缺点:所有实例都会共享父类的引用类型属性

    6.寄生组合式继承:寄生+组合实现继承

    function Child(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
    }
    var c = new Child(Parent.prototype);
    function ChildObject(name, age) {
        Parent.call(this, name);
        this.age = age;
    }
    ChildObject.prototype = c;
    c.constructor = ChildObject;
    var co = new ChildObject('赵四', 20);
    console.log(co);
    

    优点:

    1. 可以多重继承
    2. 解决调用两次父类构造函数的问题
    3. 解决实例共享父类引用类型属性的问题

    深拷贝与浅拷贝

    区分浅拷贝与深拷贝:假设 B 复制了 A,如果修改 B,A 也发生变化,就是浅拷贝;如果 A 没有发生变化,就是深拷贝

    实现浅拷贝

    1.for...in 循环赋值(只能拷贝第一层)

    function simpleCopy(obj1) {
        let obj2 = Array.isArray(obj1) ? [] : {};
        for(let k in obj1){
            obj2[k] = obj1[k];
        }
        return obj2;
    }
    let obj1 = {
        a: 1,
        b: 2,
        c: {
            d: 3
        }
    };
    let obj2 = simpleCopy(obj1);
    obj2.a = 3;
    obj2.c.d = 4;
    console.log(obj1.a); // 1
    console.log(obj1.c.d); // 4
    

    2.Object.assign

    let obj2 = Object.assign(obj1);
    obj2.a = 3;
    obj2.c.d = 4;
    console.log(obj1.a); // 3
    console.log(obj1.c.d); // 4
    

    3.直接用 =赋值

    let obj2 = obj1;
    obj2.a = 3;
    obj2.c.d = 4;
    console.log(obj1.a); // 3
    console.log(obj1.c.d); // 4
    

    实现深拷贝

    1.递归拷贝所有层级属性

    let obj1 = {
        a: 1,
        b: 2,
        c: {
            d: [1,2,3]
        },
        f: function () {
            console.log('f');
        }
    };
    function deepCopy(obj1) {
        let obj2 = Array.isArray(obj1) ? [] : {};
        for(let k in obj1){
            if(typeof obj1[k] === "object"){
                obj2[k] = deepCopy(obj1[k])
            }else{
                obj2[k] = obj1[k];
            }
        }
        return obj2;
    }
    let obj2 = deepCopy(obj1);
    console.log(obj2);
    obj2.c.d.push(4);
    console.log(obj1.c.d); // [1,2,3]
    

    2.通过 JSON 对象来实现深拷贝

    function deepCopy(obj1) {
        let obj = JSON.stringify(obj1);
        return JSON.parse(obj);
    }
    let obj2 = deepCopy(obj1);
    

    缺点:

    1. 不能复制 function、正则、Symbol
    2. 循环引用报错
    3. 相同的引用会被重复复制

    3.通过jQuery的extend方法实现深拷贝

    var array = [1,2,3,4];
    var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝
    

    4.lodash函数库实现深拷贝

    let result = _.cloneDeep(test)
    

    14、函数防抖和节流

    函数防抖和节流是为了解决用户在某一时间内频繁提交请求,给服务器造成压力的情况

    函数防抖:在一定时间内,连续触发同一事件,只执行一次(只在最后一次执行或第一次执行)

    定时器实现 :

    // 只在最后一次执行
    function debounce(fn, delay) {
        let timer = null;
        return () => {
            if(timer){
                //第一次触发时不会执行,后续触发时会清除定时器
                clearTimeout(timer);
            }
            timer = setTimeout(() => {
                fn.apply(this, arguments);
            }, delay)
        }
    }
    
    // 只在第一次执行
    function debounce(fn, delay) {
        let timer = null;
        return () => {
            if(timer){
                clearTimeout(timer);
            }
            let callNow = !timer; // true
            // 后续如果没有触发,则将timer初始化为null
            timer = setTimeout(() => {
                timer = null;
            }, delay);
            if(callNow){
                fn.apply(this, arguments);
            }
        }
    }
    

    时间戳实现:

    function debounce(fn, delay) {
        let pre = 0;
        return () => {
            let now = new Date();
            if(now - pre > delay){
                fn.apply(this, arguments);
            }
            pre = now;
        }
    }
    

    函数节流:在单位时间内,连续触发同一事件,只执行一次

    定时器实现:

    function throttle(fn, delay) {
        let flag = true;
        return () => {
            if(!flag) return; // flag为false时,直接返回
            flag = false;
            setTimeout(() => {
                fn.apply(this, arguments);
                // 执行完fn函数再将flag重置为true
                flag = true;
            }, delay)
        }
    }
    
    // 或者
    function throttle(fn, delay) {
        let timer = null;
        return () => {
            if(!timer){ // timer为null时才设置定时器
                timer = setTimeout(() => {
                    //执行完fn函数后将timer重置为null
                    fn.apply(this, arguments);
                    timer = null;
                },delay)
            }
        }
    }
    

    时间戳实现:

    function throttle(fn, delay) {
        let pre = 0;
        return () => {
            let now = new Date();
            if(now - pre > delay){
                fn.apply(this, arguments);
                pre = now;
            }
        }
    }
    

    15、DOM常见的操作方式

    1.查找节点

    document.querySelector(selectors)
    //接受一个CSS选择器为参数,返回第一个匹配该选择器的元素节点
    document.querySelectorAll(selectors)
    //接受一个CSS选择器为参数,返回所有匹配该选择器的元素节点
    document.getElementById(id)
    //返回匹配指定id属性的元素节点
    

    2.生成节点

    document.createElement(tagName) 
    // 用来生成HTML元素节点
    document.createTextNode(text) 
    // 用来生成文本节点
    document.createAttribute(name) 
    // 生成一个新的属性对象节点,并返回它
    

    3.事件操作

    document.addEventListener(type,listener,capture) // 注册事件
    document.removeEventListener(type,listener,capture) // 注销事件
    

    4.节点操作

    Node.appendChild(node) 
    // 向节点添加最后一个子节点
    Node.hasChildNodes() 
    // 返回布尔值,表示当前节点是否有子节点
    Node.cloneNode(true); 
    // 默认为false(克隆节点), true(克隆节点及其属性,以及后代)
    Node.insertBefore(newNode,oldNode) 
    // 在指定子节点之前插入新的子节点
    Node.removeChild(node) 
    // 删除节点,在要删除节点的父节点上操作
    Node.replaceChild(newChild,oldChild) 
    // 替换节点
    
  • 相关阅读:
    Java方向如何准备技术面试答案(汇总版)
    HTTP与HTTPS的区别
    MyBatisPlus环境下使用MyBatis的配置类
    idea 插件的使用 进阶篇(个人收集使用中的)
    Leveldb实现原理
    浅析 Bigtable 和 LevelDB 的实现
    IKAnalyzer进行中文分词和去停用词
    Elasticsearch之中文分词器插件es-ik的自定义词库
    使用Java High Level REST Client操作elasticsearch
    任正非:坚持逐渐辞退低绩效员工
  • 原文地址:https://www.cnblogs.com/codeDD/p/13369719.html
Copyright © 2011-2022 走看看