前端面试3.2-JS

目录

1、内存泄漏

2、箭头函数

3、声明和定义

4、垃圾回收机制

4、不建议在JS中使用innerHTML

5、null和undefined的区别

6、new操作符的作用

7、函数作用域和块作用域

1、内存泄漏

        内存泄漏就是内存中的变量没有回收,一直存在与内存中,造成内存的浪费的行为。

        1)意外的全局变量; 2)计时器和回调函数timers; 3)DOM泄漏;4)js闭包; 5)console

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p id="p1">p1</p>
    <!-- 内存泄漏:内存中的变量没有回收,一直存在与内存中,造成内存的浪费 -->
    <script>
      // 1. 意外的全局变量 这里的a就是全局变量 它是foo()内部作用域变量的引用
      function foo() {
        // 一个未声明变量的使用,会在全局对象中创建一个新的变量
        a = "test";
        console.log(a);
      }
      // 上面的写法等价于
      // function foo() {
      //     window.a = "test";
      // }
      foo();
      // 1.解决方法;清空这个变量 不过有缓存问题
      a = null;
      console.log(a);
    </script>
    <script>
        // 2.解决方法:在js文件开头添加 ‘use strict',开启严格模式 不存在意外的全局变量
        'use strict'
      function foo() {
        // 必须定义
        let a = "test";
        console.log(a);
      }
      foo()
      // 这里的a变为局部变量
      // console.log(a);
      // 3.定时器setInterval或者setTimeout使用没有及时清理
      // 定义一个叫timer的定时器
      const timer=setInterval(()=>{
        console.log(1);
      },200)
      // 清除叫timer的定时器
      clearInterval(timer)
      // 3. DOM泄漏
      // 1)给DOM对象添加的属性是一个对象的引用
      const obj={
        a:1
      }
      // 给p1添加一个c属性 浅拷贝了对象obj给c obj变了c也跟着变化
      document.getElementById('p1').c=obj
      obj.d=5
      console.log(document.getElementById('p1').c);
      // 解决方法:在window.onload时间中加上 document.getElementById('id').c = null;
      window.onload=()=>{
        document.getElementById('p1').c = null; 
        console.log(document.getElementById('p1').c);
      }
      // 2)元素引用没有清理 直接删除元素
      // 3)事件的绑定没有移除
      const fn=()=>{
        console.log(1);
      }
      document.getElementById('p1').addEventListener('click',fn)
      removeEventListener('click',fn)
      // 控制台打印的没有清除
      // js闭包
      // 闭包函数里面的局部变量没有销毁
    </script>
  </body>
</html>

2、箭头函数

        箭头函数是匿名函数,不能作为构造函数;

        不会创建自己的this,只会从自己的作用域链的上一层继承this;不能通过apply和call改变this

        不能使用arguments,用rest参数解决;

        没有原型prototype,,没有super用于访问原型属性        

        不可以使用yield,不能用作Generator函数

3、声明和定义

        变量声明不开辟内存,只是告诉编译器要声明的部分存在,要预留部分的空间;var i 

        变量定义开辟内存;

4、垃圾回收机制

        浏览器的js具有自动垃圾回收机制,垃圾回收机制也就是自动内存管理机制

        垃圾:(1)没有被引用的对象或变量;(2)无法访问到的对象(几个对象引用形成一个环,互相引用)

        可达性:指那些以某种方式可以访问到或可以用到的值,它们被保证存储在内存中

        垃圾回收机制(GC Garbage Collection):执行环境负责管理代码执行过程中使用的内存。JS的垃圾回收机制是为了以防内存泄漏(当已经不需要某块内存时这块内存还存在着,没有被释放,导致该内存无法被使用),垃圾回收机制就是间歇的不定期的寻找不再使用的变量,并释放掉它们所指向的内存;

        JS中检测哪块内存可以被回收的方法:标记清除、引用计数

        1)标记清除 最常用的垃圾回收方式

                ①当变量进入执行环境时(函数中声明变量),就标记这个变量为“进入环境”,当变量离开环境时(函数执行结束),则将其标记为“离开环境”

                ②垃圾回收器再运行的时候会给存储在内存中的所有变量都加上标记;

                ③去掉环境中的变量以及被环境中的变量引用的变量(闭包)的标记

                ④之后还存在标记的变量即是需要回收的变量

                ⑤最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间

        2)引用计数  

                跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1;如果该变量的值变成另外一个,则这个值的引用次数减1;当这个值的引用次数变为0时,说明这个值没有办法被访问,被视为准备回收的对象;每当过一段时间开始垃圾回收时,就把引用次数为0的值所占用的空间清理;

                引用计数方法可能导致循环引用,类似死锁,导致内存泄漏。

4、不建议在JS中使用innerHTML

        通过innerHTML修改内容,每次都会刷新,因此很慢;在innerHTML中没有验证的机会,因此更容易在文档中插入错误代码,使网页不稳定。

5、null和undefined的区别

        null是一个表示“无”的对象,转为数值时为0;

        undefined是一个表示“无”的原始值,转为数值时为NaN

6、new操作符的作用

        1)创建一个空对象

        2)将新对象与构造函数通过原型链连接(obj.__proto__ = fn.prototype)

        3)将构造函数的this绑定到新对象上(fn.apply(obj,args))

        4)根据构造函数的返回类型做判断,如果是值,返回对象obj;如果是引用类型,返回这个引用类型的对象

function newFunc(fn,...args) {
    // 1.创建一个新对象
    let obj = {}
    // 2.将新对象和构造函数通过原型链连接
    obj.__proto__ = fn.prototype
    // 3.将构造函数的this绑定到新对象上
    const result = fn.apply(obj,args)
    // 4.根据返回值类型判断,如果是值类型返回obj,如果是引用类型返回正常引用类型
    return result instanceof Object ? result : obj   

7、函数作用域和块作用域 

        函数作用域:属于这个函数的任何声明(变量和函数)都可以在这个函数的范围内使用及复用(包括这个函数嵌套的作用域)

        块级作用域:当代码块中存在let或const,这个代码块就成为块级作用域(定义变量和使用变量只能在同一个{}内);

8、JS中的继承方式

        继承可以使得子类具有父类的各种方法和属性;

        1)原型链继承

                构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针 

      • 定义父类型构造函数
      • 给父类型的原型添加方法
      • 定义子类型的构造函数
      • 创建父类型的实例对象赋值给子类型的原型
      • 将子类型原型的构造属性设置为子类型
      • 创建子类型的对象:可以调用父类型的方法
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

function SubType() {
    this.subproperty = false;
}

// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType(); 
// 让子类型的原型的constructor指向子类型
SubType.prototype.constructor = SubType
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true

        缺点:多个实例对引用类型的对象会被篡改

        2)借用构造函数继承

        使用父类的构造函数来增强子类的实例,等同复制父类的实例给子类

      • 定义父类型构造函数
      • 定义子类型构造函数
      • 在子类型构造函数中调用父类型构造
function  SuperType(){
    this.color=["red","green","blue"];
}
function  SubType(){
    //继承自SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();//SubType创建子类实例时调用SuperType构造函数,SubType的每个实例都会将SuperType中的属性复制一份
alert(instance2.color);//"red,green,blue"

        缺点:

                只能继承父类的实例属性和方法,不能继承原型属性和方法

                无法实现复用,每个子类都有父类实例函数的副本,影响性能

        3)组合继承

                用原型链实现对原型属性和方法的继承,用借用构造函数实现实例属性的继承;

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

        4)原型式继承:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型 

  let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };
 
  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");
  let person5 = Object.create(parent4);
  person5.friends.push("lucy");
 
  console.log(person4.name); //tom
  console.log(person4.name === person4.getName());//true
  console.log(person5.name);//parent4
  console.log(person4.friends);//["p1", "p2", "p3","jerry","lucy"]
  console.log(person5.friends);["p1", "p2", "p3","jerry","lucy"]

        Object.create()接收两个参数,第一个参数是作为新对象原型的队形,第二个可选参数是为新对象定义额外属性的对象,该方法只能实现浅拷贝,所以最后两个输出结果一样 

        5)寄生式继承 

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

   let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };
 
  function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
      return this.friends
    };
    return clone;
  }
 
  let person5 = clone(parent5);
  console.log(person5.getName());
  console.log(person5.getFriends());

        6)寄生组合式继承 

  function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }
 
  function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
  }
   Parent6.prototype.getName = function () {
    return this.name;
  }
  function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
  }
 
  clone(Parent6, Child6);
 
  Child6.prototype.getFriends = function () {
    return this.friends;
  }
 
  let person6 = new Child6();
  console.log(person6);
  console.log(person6.getName());
  console.log(person6.getFriends());

        7)ES6中extends关键字实现

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}
 
class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}
 
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

9、深拷贝和浅拷贝

        数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。

  • 基本数据类型的特点:直接存储在栈(stack)中的数据
  • 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

        浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

        浅拷贝和赋值的区别:

  •         当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
  •         浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

赋值

// 对象赋值
 var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

浅拷贝

// 浅拷贝
 var obj1 = {
    'name' : 'zhangsan',
    'age' :  '18',
    'language' : [1,[2,3],[4,5]],
};
 var obj3 = shallowCopy(obj1);
 obj3.name = "lisi";
 obj3.language[1] = ["二","三"];
 function shallowCopy(src) {
    var dst = {};
    for (var prop in src) {
        if (src.hasOwnProperty(prop)) {
            dst[prop] = src[prop];
        }
    }
    return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)

实现浅拷贝的方式:1)Object.assign({},obj) 当obj中的属性都为基本数据类型时为深拷贝

                                2) arr.concat()    3)arr.slice()

实现深拷贝的方式:1)JSON.parse(JSON.stringify(obj))

                        用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。但不能处理函数

let arr = [1, 3, {
    username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

                                2)ES6展开语法 {...obj}

                                3)手写递归

function clone(origin){
    let obj={};
    if(origin instanceof Array){
        obj=[];
    }
    for(let key in origin){
        let value=origin[key];
        obj[key]=(typeof value==="object" && value !== null)?clone(value):value 
    }
    return obj
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值