JS 高级(面试必会知识点)

目录

一、对象与 JSON 的区别?

1、怎么让对象对自身进行深拷贝

二、怎么判断js类型

通过 typeof 判断。

用 constructor 构造函数的方式,判断某个实例对象是不是这个构造函数创建的。

通过Object.prototype.toString.call()。最全面的

通过 instanceof 进行判断

三、 this指向的问题

四、改变 this 指向的方法

 五、new 的关键字用什么作用

六、什么是构造函数?

1、Function内置的构造函数和Object内置的构造函数他们之间的原形链的关系什么?

2、JS 面向对象的内置构造函数

Object:用于创建对象

Function:创建一个方法

Array:用于构造数组

Number:

String:

Boolean

七、原型、原型对象、原型链

八、继承

1、原型链继承

2、构造函数继承(借助 call)

3、组合继承(前两种的组合)

4、原型式继承(Object.create)

5、寄生式继承 

 6、寄生组合式继承

九、JS的执行机制

十、闭包

1、什么是闭包 

2、闭包的作用

3、闭包用途

4、使用闭包的注意点 

5、经典面试题 



一、对象与 JSON 的区别?

  • JSON 是对象的一个严格的子集
  • JOSN 中的key 必须加 " "
  • JSON 不能赋值
  • 使用JSON.parse() 会把JSON数据中值为undefined的数据过滤掉

1、怎么让对象对自身进行深拷贝

    function ss(ob) {
            let obj = {}
            for (let a in ob) {
                if (ob[a] === ob) return Object.assign({}, ob)
            }
        }
        var dd = { name: '搜索', age: 16 }
        // 通过它触发改变
        Object.defineProperty(dd, 'age', {
            get() { return this }
        })

        let aa = ss(dd)
        console.log('aa', aa);
        console.log('判断', aa === dd);

二、怎么判断js类型

  • 通过 typeof 判断。

            console.log(typeof 2);   // number
            console.log(typeof '字符');   // string
            console.log(typeof true);   // boolean
            console.log(typeof null);   // object
            console.log(typeof undefined);   // undefined
            console.log(typeof Symbol(3));   // symbol
            console.log(typeof function(){}); // function
            console.log(typeof NaN);   // number
            console.log(typeof {name:'老六',age:66}); // object
            console.log(typeof ['1',2,'3']); // object

    用来判断基本数据类型,难以判断函数除外的复杂数据类型

  • 用 constructor 构造函数的方式,判断某个实例对象是不是这个构造函数创建的。

            let a = 2
            let b = '字符'
            let c = true
            let obj = {name:'老六',age:66}
            let arr = ['1',2,'3']
            let fun = function(){}
    
            console.log(a.constructor == Number);   // true
            console.log(b.constructor == String);   // true
            console.log(c.constructor == Boolean);   // true
            console.log(obj.constructor == Object);   // true
            console.log(arr.constructor == Array);   // true
            console.log(fun.constructor == Function); // true

    原理:因为构造函数创建的时候,会有一个prototype属性指向原型对象,而原型对象中会有一个constructor属性指回来构造函数

  • 通过Object.prototype.toString.call()。最全面的

    console.log(Object.prototype.toString.call(2));   // [object Number]
    console.log(Object.prototype.toString.call('字符'));   // [object String]
    console.log(Object.prototype.toString.call(true));   // [object Boolean]
    console.log(Object.prototype.toString.call(null));   // [object Null]
    console.log(Object.prototype.toString.call(undefined));   // [object Undefined]
    console.log(Object.prototype.toString.call(Symbol(3)));   // [object Symbol]
    console.log(Object.prototype.toString.call(function(){}));   // [object Function]
    console.log(Object.prototype.toString.call(NaN));   // [object Number]
    console.log(Object.prototype.toString.call({name:'老六',age:66})); // [object Object]
    console.log(Object.prototype.toString.call(['1',2,'3'])); // [object Array]

    必须是Object原型对象上的toString方法才能判断,其他原型对象上的不能判断。

    并且进行数据类型判断,我们需要借助apply方法或者call方法或者bind方法。

    例如:Array原型对象上的toString方法是判断不了的,因为它重写了toString方法。

    注意:判断不了自定义构造函数创建出来的实例对象

  • 通过 instanceof 进行判断

            console.log(function(){} instanceof Function);   // true
            console.log(new Date instanceof Date);   // true
            console.log({name:'老六',age:66} instanceof Object); // true
            console.log(['1',2,'3'] instanceof Array); // true

    instanceof:检查一个对象(引用类型)是否为构造函数(类)的实例

    可以用来判断内置对象

三、 this指向的问题

  • 方法() 调用 this指向window
  • 一个函数通过对象打点调用,this指向这个对象
  • 事件处理函数,谁触发this就指向谁
  • 定时器里面的匿名函数this指向window
  • 一个方法在数组里面通过下标的方式去调用,this指向数组,

总结:谁调用 this就指向谁

四、改变 this 指向的方法

手写实现

call():可以调用函数,同时改变this的指向,只能传字符串形式参数

apply():可以调用函数,同时改变this指向,只能传数组形式参数

bind() :不能调用函数,它是重新定义一个函数并返回,只能传字符串形式参数

        function fun(a,b){
            console.log('this',this);
            console.log(a,b);
            // 调用fun(obj,'普通'),返回undefined,因为window身上没有age
            console.log(this.age);  
        }

        var obj = {
            name: '小明',
            age:5
        }
        
        fun(obj,'普通')   // this 指向window
        fun.call(obj,3,333)      // this 指向obj
        fun.apply(obj,[6,666])   // this 指向obj
        fun.bind(obj)(9,999)     // this 指向obj

 五、new 的关键字用什么作用

  • 在函数内部(内存)创建了一个局部变量,是一个空的对象

  • 构造函数中的this(上下文)指向这个空的对象

  • 在这新对象中添加一个__proto__属性,指向构造函数的原型对象prototype

  • 所有的语句执行完毕,函数将自动return这个新对象(this),如果是引用类型,就返回这个引用类型的对象(值)

 手写流程:

// 手写new
function newFunc(Func,...args) {
    // 1.创建一个新对象
    let newObj = {}
    // 2.将新对象和构造函数通过原型链连接
    newObj.__proto__ = Func.prototype

    /*
      注意:可以通过【Object.create(新创建对象的原型对象)】,来简化第一、二步骤
      let newObj = Object.create(Func.prototype)
    */

    // 3.将构造函数的this绑定到新对象上
    const result = Func.apply(newObj,args)
    // onsole.log(result) // 值为undefined
    // 4.根据返回值类型判断,,如果是引用类型直接返回值,否则返回this(创建的新对象)
    return result instanceof Object ? result : newObj   
}

// 测试
function Person(name, age) {
    this.name = name;
    this.age = age;
    // 若没有return 相当于 return undefined
    // return {sex:'男'}  // 返会引用类型
}
// 添加往Person原型对象上添加一个sayName方法
Person.prototype.sayName = function (){
    console.log('名字:',this.name);
}
const person1 = newFunc(Person, 'Tom', '18')
console.log(person1);
person1.sayName()

六、什么是构造函数?

  • 当一个函数被new调用的时候,这个函数就是一个构造函数
  • 构造函数就是一个普通的函数,里面可以写任何的语句,只不过this指向的是一个对象

  • 构造函数里面写了return的语句会发生两种不同的处理方式。如下:

  1. 如果return 基本数据类型(number null boolean string undefined)的话,程序会被打断执行
  2. 如果return 引用数据类型(Array Date Function Object。。。),那么return的值会覆盖掉原来的值

1、Function内置的构造函数和Object内置的构造函数他们之间的原形链的关系什么?

  • 任何的函数都是Function 的实例对象
  • Object也是Function new出来的实例对象
  • Function 自己也是自己的实例对象
  • 任何对象都是Object的实例对象。

2、JS 面向对象的内置构造函数

  • Object:用于创建对象

字面量形式创建的对象其实都是Object new的实例对象。

  • Function:创建一个方法

字面量创建的函数都是Function new出来的实例对象

  • Array:用于构造数组

任何的数组的字面量的形式的对象,都是Array new出来的实例对象

  • Number:

任何字面量创建number类型都是Number new出来的实例对象

 注意:使用 Number 构造函数创建的对象,也可以进行计算,但有时候有坑

        var a = new Number(9)
        console.log(a);
        if(a) alert('success')
        else alert('fail')
  • String:

字符串有很多方法,这些方法都是定义在String的prototype上面(原形上面)

  • Boolean

Var  a=true

Var a=new Boolean(true)

总结: 所有的内置的对象,其实都是内置的构造函数

七、原型、原型对象、原型链

Prototype:每一个构造函数都有属性,指向一个空的对象(原型)

Prototype 不需要去定义,天生就有

People.prototype 是People 构造函数的原型

People.prototype 是小明或者小红的原型对象

原型链:任何一个构造函数都有一个属性叫做prototype指向了一个对象,当这个构造函数被new出来的时候,它的每一个实例的__proto__属性 也指向这个对象

 __proto__:原型链查找的功能,当小明或者小红没有这个属性或者方法的时候,她就会沿着原型链往上找,直到确定原型对象是否有这个属性或者方法。

Constructor属性:任何一个构造函数的prototype身上都有一个 constructor属性指向他的构造函数

        function person(name, age, gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
        }
        person.prototype.toString = function () {
            console.log('aaa');
            return "person[" + this.name + "," + this.age + "," + this.gender + "]"
        }
        var a = new person("猪八戒", 29, "男");
        console.log(a);   //一个实例化对象,a
        a.toString()      // 打印 aaa
        console.log(a.__proto__.constructor);  // 打印 person这个函数

instanceof运算符:用来检测一个对象是不是某一个构造函数的实例

console.log(对象 instanceof 构造函数);

 总结:函数的prototype指向谁,函数new出来的玩意的__proto__就指向谁

面试题:

八、继承

1、原型链继承

缺点:多个实例化对象共享一个原型对象,一个发送改变,另一个也随着改变

       function Person() {
            this.name = 'xiangming'
            this.age = [1, 2, 3]
        }
        Person.prototype.shuohua = function () {
            console.log("说话")
        }
        function obj() {
            this.type = '老六'
        }

        obj.prototype = new Person()
        var a = new obj()
        console.log(a);
        a.shuohua()

2、构造函数继承(借助 call)

优点:解决原型链继承不能传递参数给父类

缺点:父类定义方法只能定义在父类的构造函数里面,不能定义在原型上

总结:只能继承父类的实例属性和方法,不能继承原型属性或者方法。

        function Person(age){
            this.name = '小明'
            this.age = age
            this.sayName = function(){
                return this.name
            }
        } 
        // 不能定义在外面
        // Person.prototype.sayName = function(){
        //     return "this.name"
        // }

        function Chinese(name,age){
            Person.call(this,666)
        }

        var xiaoming = new Chinese(666)
        console.log(xiaoming);
        console.log(xiaoming.sayName());  // 小明

3、组合继承(前两种的组合)

原型链继承与构造函数继承的组合

优点:能够解决原型链继承与构造函数继承的问题

缺点:构造函数多执行了一遍,另外进行了一次性能开销

        function Person() {
            this.name = '老六';
            this.play = [1, 2, 3];
        }
        // 往Person原型上添加方法
        Person.prototype.getName = function () {
            return this.name;
        }
        function Child3() {
            // 第二次调用 Person()
            Person.call(this);
            this.type = 'child3';
        }

        // 第一次调用 Person()
        Child3.prototype = new Person();
        // 手动挂上构造器,指向自己的构造函数
        Child3.prototype.constructor = Child3;
        var s3 = new Child3();
        var s4 = new Child3();
        s3.play.push(4);
        console.log(s3.play, s4.play);   // 不互相影响
        console.log(s3.getName());   // 正常输出'老六'
        console.log(s4.getName());   // 正常输出'老六'
        console.log(Child3.prototype.constructor);

4、原型式继承(Object.create)

Object.create()方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。可以实现浅拷贝的作用

缺点:

  • 多个实例化对象共享一个原型对象,一个发送改变,另一个也随着改变,存在篡改的可能
  • 父类定义方法只能定义在父类的构造函数里面,不能定义在原型上
  • 拷贝后的对象(子类)不能直接添加方法,需要添加到原型对象上
        let Person={
            name : 'xiangming',
            age : [1,2,3],
            getName: function () {
                console.log(this.name);
            }
        }

        let obj1 = Object.create(Person)
        obj1.age.push(4)
        let obj2 = Object.create(Person)
        obj2.age.push('44')

        obj1.str=function(){
            console.log('我是新增的方法');
        }
        console.log('obj',obj1.age);  //[1,2,3,4,'44']
        console.log('obj',obj2.age);  //[1,2,3,4,'44']
        console.log('obj',obj1.__proto__); // Person
        obj1.getName() // xiangming
        obj2.str()   // 报错。需要添加到原型对象上 obj1.__proto__.str


       //【Object.create的实现原理?】
       function obj_create(o){
            function F(){}  //构造函数
            F.prototype = o; // 把F的原型替换为传递过来的Person
            return new F()
        }
        let Person={
            name : 'xiangming',
            age : [1,2,3]
        }
        let AA = obj_create(Person) // AA 是F的实例化对象
        console.log(AA); // 可实现原型链的向上查找功能

5、寄生式继承 

寄生式继承:使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法。

优缺点和原型式继承一样

        function obj_create(o) {
            function F() { }  //构造函数
            F.prototype = o; // 把F的原型替换为传递过来的Person
            return new F()
        }

        function obj(o) {
            var chone = obj_create(o)
            chone.sayHi = function () {
                console.log('hi')
            }
            return chone
        }

        var Person = {
            name: "jill",
            frindes: ["sk", "xiaoming", "xiaogang"],
            age: function () {
                console.log(this.name);
            }
        }
        var A = obj(Person)
        console.log(A);
        A.sayHi()
        A.age()

 6、寄生组合式继承

结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

九、JS的执行机制

  • 同步任务:代码从上到下顺序执行
  • 异步任务:又分为 宏任务 与 微任务
  1. 宏任务:script(整体代码)、setTimeout、setInterval、UI交互事件、postMessage、Ajax
  2. 微任务:Promise.then catch finally、MutaionObserver、process.nextTick(Node.js 环境)

 运行机制:所有的同步任务都是在主进程的执行中形成一个执行栈,主进程之外还有一个”任务队列(异步任务队列)“,在这个队列中先执行宏任务,在清空(执行)当前宏任务中的所有微任务,然后进行下一个tick形成循环。

<script setup lang='ts'>
async function Prom() {
    console.log('y');
    await Promise.resolve()  // await 相对于微任务
    console.log('x');  // 相当于写在Promise.then里面
}
setTimeout(()=>{
    console.log('1');
    Promise.resolve().then(()=>{
        console.log('2');
    })
},0)
setTimeout(()=>{
    console.log('3');
    Promise.resolve().then(()=>{
        console.log('4');
    })
},0)
Promise.resolve().then(()=>{
    console.log('5');
})
Promise.resolve().then(()=>{
    console.log('6');
})
Prom()
console.log(0);
// console.log('y 0 5 6 x 1 2 3 4'); // 正确输出结果
</script>

十、闭包

闭包前我们需要清楚自执行函数、函数作用域、内存回收机制、作用域继承 

  • 自执行函数:可以无需调用,自动执行函数,传参方便
    /* 特性:1.自执行函数是很自私的,它的内部可以访问全局变量。
             2.但是除了自执行函数自身内部,是无法访问它的。
             3.自执行函数无需给函数名,因为根本没办法在其他地方调用,它本身只会执行一次   */
    
    function s1(a1,b1){
        return sum1 = a1 + b1
    }
    (function s2(a2,b2){
        console.log('内部',s2) // 打印s2函数
    	return sum2 = a2 + b2
    ;})()
    console.log(s1) // 打印s1函数
    console.log(s2) //报错:autoFunc.html:38 Uncaught ReferenceError: s2 is not defined
    
    
    /* 总结:1.能够实现作用域的绝对隔离和函数命名冲突
             2.主要用于闭包和创建独立的命名空间两个方面
             3.自执行函数将某些代码包裹起来可以实现块级作用域的效果,减少全局变量的数量
             4.自执行函数执行结束后变量就会被内存释放掉,从而也会节省了内存。  */
  • 函数作用域:函数要执行时就会在内存里面创建一个独立作用域————封闭的盒子。
    /*在函数执行完毕,这个独立作用域就会删除。
    有一种情况下这个封闭的盒子是不会删除的,那就是“闭包”,*/
    
    // 函数执行
    function fn(){
        var a = 1
    }
    // 函数执行完毕
  • 内存回收机制:把用不到的内容空间,系统会自动清理回收提供给其它程序使用,
    /*内部函数引用外部的函数的变量,外部函数执行完毕,作用域也不会删除。
    从而形成了一种不删除的独立作用域。*/
    
    function fn(){
       // b的独立作用域是不删除的,因为被内部函数引用了
       let b = 2
       return function(){
           // 内部函数引用了外部函数的变量 b
           console.log(b)
       }
    }

    某一个变量或者对象被引用,因此在回收的时候不会释放它,因为被引用代表着被使用,回收机制不会对正在引用的变量或对象进行回收的。

  • 作用域继承:在子作用域中可获取父作用域中的东西,但父作用域获取不到子作用域的东西

1、什么是闭包 

function fn(){
   // b的独立作用域是不删除的,因为被内部函数引用了
   let b = 2
   return function(){
       // 内部函数引用了外部函数的变量 b
       console.log(b)
   }
}

在一个函数里边再定义一个函数。这个内部函数一直保持有对外部函数中作用域的访问(小盒子可以一直访问大盒子但大盒子不能访问小盒子)。

函数执行,形成一个独立作用域,保护里边的私有变量不受外界的干扰,除了保护私有变量外,还可以存储一些内容,这样的模式叫做闭包。

2、闭包的作用

 通过一系方法,将函数内部的变量(局部变量)转化为全局变量

3、闭包用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

       function f1() {
            var n = 999;
            nAdd = function () {
                alert(n += 1);
            }
            function f2() {
                alert(n);
            }
            return f2;
        }
        var result = f1();
        result(); //999
        nAdd();//1000
        result(); //1000

注意:上面代码,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。 其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个操控者,可以在函数外部对函数内部的局部变量进行操作。

4、使用闭包的注意点 

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

5、经典面试题 

for (var i = 1; i < 5; i++) {
    setTimeout(function () {
        console.log(i);  // 5 5 5 5 
    }, 0);
}

由于JavaScript是单线程的,setTimeout 是异步任务,JS会将其放入到任务队列中,待同步任务执行完毕后,才执行任务队列中的异步任务。

因为setTimeout函数也是一种闭包,往上找它的父级作用域链(window),因为变量 i 是用 var 声明的全局变量,会被挂载到 window 上,所以变量i的值变成了i = 5,最后执行setTimeout时输出4个5

 解决方案:

  • 使用立即执行函数锁定参数值,把每次循环的索引传入,从而锁定索引值
    for (var i = 1; i < 5; i++) {
        (function (i) {
            setTimeout(function () {
                console.log(i);
            }, 0);
        })(i);
    }
    
  •  使用 let 声明(块级作用域)
    // 使用块级作用域变量关键字,会为每次循环创建独立的变量,从而每次打印都会有正确的索引值。
    
    for (let i = 1; i <= 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, 0);
    }
    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值