javaScript总结(6-10)

六、深拷贝浅拷贝的区别?如何实现一个深拷贝?

1.数据类型存储
基本类型存储在栈内存中
引用类型的引用地址(指针)存储在栈内存中,数据保存在堆内存中。
2.浅拷贝
指创建新的数据,这个数据与原始数据属性值的精确拷贝
如果是基本类型,拷贝的就是基本类型的值
如果是引用类型,拷贝的就是内存地址
存在浅拷贝现象的有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

3.深拷贝
深拷贝会开辟一个新的栈,两个对象属性完全相同,但是引用地址不同,修改一个对象的属性不会引起另一个对象属性的变化
常见的深拷贝方式:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

4.区别
浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

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

小结:浅拷贝对基本数据类型可以精确拷贝,对引用数据类型拷贝的只是内存地址,所以当我们在修改对象中的属性时,会将原始对象的数据也发生改变。而深拷贝可以创建一个一模一样的新对象来存储数据,在修改对象数据时不会对原始数据做出改变。浅拷贝通常在使用Object中的assign方法,Array的是splic和concat方法以及拓展运算符复制时出现,进行深拷贝可以通过lodash中的clonedeep方法,jQuery的etend方法,JSON.stringify()以及自写一个循环深拷贝方法。

七、说说你对闭包的理解?闭包使用场景?

1.是什么
一个函数和对其周围的状态的引用捆绑在一起,这样的组合就是闭包。
也就是说,闭包可以让我们在一个内层函数访问到外层函数的作用域(可以使用父作用域的变量等)
2.使用场景

  • 创建私有变量
  • 延长变量的生命周期
  • 柯里化函数
    柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用
  • 使用闭包模拟私有方法
    在JavaScript中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法
  • 防抖和节流

小结:闭包就是一个函数和其周围状态的引用捆绑在一起,使用闭包可以使我们在内层函数访问外层函数的作用域,使用场景有创建私有变量、函数柯里化、模拟私有方法、防抖和节流等

八、说说你对Javascript中作用域的理解?

1.作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性
我们一般将作用域分成:

  • 全局作用域
  • 函数作用域
  • 块级作用域

全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

块级作用域
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

词法作用域

词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript遵循的就是词法作用域

由于JavaScript遵循词法作用域,相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以输出2

作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

小结:作用域就是变量或函数能被访问的区域或集合,一般分为全局作用域、函数作用域和块级作用域,全局作用域就是不在函数或大括号中的作用域,在全局作用域声明的变量可以在程序任何位置访问,函数作用域在声明的函数中,在函数中声明的变量只能在函数内部访问,块级作用域中由es6引入的let,const声明的变量只能在块级作用域中访问,
作用域链
在js中使用变量时,js引擎会先尝试在当前作用域寻找、如果没找到会到上层作用域中寻找,以此类推直到找到或访问到全局作用域,如果还未找到(在非严格模式下会隐式声明该变量)或直接报错。

九、JavaScript原型,原型链 ? 有什么特点?

1.原型
JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

2.原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
参考:https://juejin.cn/post/6984678359275929637

小结:在js中创建一个函数时,就会生成一个属性prototype,该属性就是此函数的原型对象,在该对象中有个属性为constructor,用来指向该函数,这样原型对象和它的函数就联系了起来,通过这个函数创造出来的所有实例的原型__proto__就是该原型对象。
原型链就是,我们在创造出来的实例中可以访问它的构造函数的原型对象中的方法,当我们访问某个属性或方法时,他会先在自己构造函数的原型对象prototype上寻找,如果没找到,就继续向该原型对象的原型__proto__中寻找,这样一层一层向上查找形成的链式结构就称为原型链
原型链的尽头是构造函数function Object的原型对象,在该原型对象中的__proto__指行null,所以原型链的尽头为null

十、说说Javascript中的继承?如何实现继承?

1.是什么
继承(inheritance)是面向对象软件技术当中的一个概念

如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”

继承的优点
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能

虽然JavaScript并不是真正的面向对象语言,但它天生的灵活性,使应用场景更加丰富
2.实现方式

  • 原型链继承
    将子类构造函数的原型对象指向父类构造函数的实例,这样该子类构造函数就可以调用父类构造函数原型对象的属性或方法, 缺点:改变子类构造函数继承的属性,父类构造函数的属性也改变
 function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child() {
    this.type = 'child2';
  }
  Child.prototype = new Parent();
  console.log(new Child())
  • 构造函数继承(借助 call)
    借用构造函数继承是指在子类构造函数中调用父类构造函数,并使用 call 或 apply 方法将父类的 this 指向子类实例。
    缺点:只能继承父类实例的属性和方法,无法继承父类原型上的方法
 function Parent(){
    this.name = 'parent1';
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child(){
    Parent.call(this);
    this.type = 'child'
}

let child = new Child();
console.log(child);  // 没问题
console.log(child.getName());  // 会报错
  • 组合继承
    组合继承是指将原型链继承和借用构造函数继承结合起来。这种方式可以继承父类实例和原型上的属性和方法,但是会调用两次父类构造函数,且父类原型上的属性和方法会被继承两次。
 function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
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()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销

  • 原型式继承
    这里主要借助Object.create方法实现普通对象的继承
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方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

  • 寄生式继承
    寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法
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()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

其优缺点也很明显,跟上面讲的原型式继承一样

  • 寄生组合式继承
    寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式
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); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题

  • ES6 中的extends关键字直接实现 JavaScript的继承
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() // 成功访问到父类的方法

利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

小结:继承就是通过子类通过该方式具有调用父类的各种属性和方法,而不用重复编写相同的代码,子类继承父类的同时还应该可以重新定义某些方法,即覆盖父类的方法,使其获得和父类不同的功能。
继承的方法有原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。
拓展问题:
简单说一说:原型链继承
原型链继承就是将子构造函数的原型对象prototype指向父构造函数的实例,这样子构造函数的实例就可以访问父构造函数原型对象上的方法和属性,缺点就是子修改属性或方法时父的方法或属性也发生改变。
简单说一说:构造函数继承
构造函数继承就是通过在子构造函数中调用父构造函数,并通过call、apply将this指向为子构造函数的实例。
缺点就是无法调用父构造函数的原型对象中的方法
简单说一说:组合继承
组合继承就是将原型链继承和构造函数继承综合起来,先将子构造函数的原型对象指向父构造函数的实例,再在子构造函数中调用父构造函数并将this指向子构造函数的实例,这样互补了原型链继承和构造函数继承的缺点,但是调用了两次父构造函数,多了一次调用开销。
简单说一说:原型式继承
原型式继承就是通过Object.create方法进行继承,缺点就是该方法是一个浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改可能。
简单说一说:寄生式继承
寄生式继承是指创建一个新对象,并在该对象上增加一些父类的属性和方法,然后返回该对象。这种方式的缺点与原型式继承相同。
简单说一说:寄生组合式继承
寄生组合式继承是指使用“借用构造函数”继承父类实例的属性和方法,并将子类原型指向一个父类实例的副本。这种方式可以避免调用两次父类构造函数,且不会继承父类原型上的属性和方法。
简单说一说:类的继承
ES6 中,可以使用 class 和 extends 关键字来实现继承。即定一个父类(也称为基类)和一个子类(也称为派生类),并通过 extends 关键字让子类继承父类的属性和方法。
我们通过babel转码工具不难发现,extends本质上也是寄生组合继承。

注意:学习总结
参考链接:https://github.com/febobo/web-interview

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值