var、let 及 const 区别
涉及面试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?
对于这个问题,我们应该先来了解提升(hoisting)这个概念。
console.log(a) // undefinedvar a = 1
从上述代码中我们可以发现,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。
对于这种情况,我们可以把代码这样来看
var aconsole.log(a) // undefineda = 1
接下来我们再来看一个例子
var a = 10var aconsole.log(a)
对于这个例子,如果你认为打印的值为 undefined 那么就错了,答案应该是 10,对于这种情况,我们这样来看代码
var avar aa = 10console.log(a)
到这里为止,我们已经了解了 var 声明的变量会发生提升的情况,其实不仅变量会提升函数也会被提升。
console.log(a) // ƒ a() {}function a() {}var a = 1
对于上述代码,打印结果会是 ƒ a() {},即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升。
说完了这些,想必大家也知道 var 存在的问题了,使用 var 声明的变量会被提升到作用域的顶部,接下来我们再来看 let 和 const 。
我们先来看一个例子:
var a = 1let b = 1const c = 1console.log(window.b) // undefinedconsole.log(window. c) // undefinedfunction test(){ console.log(a) let a}test()
首先在全局作用域下使用 let 和 const 声明变量,变量并不会被挂载到 window 上,这一点就和 var 声明有了区别。
没有被挂载到window上那么它被挂载到哪里了?
它没有被挂载到window上只是说它不在作为window的一个属性而存在,它没有挂载到任何东西上,它跟window是同级别的.
再者当我们在声明 a 之前如果使用了 a,就会出现报错的情况
你可能会认为这里也出现了提升的情况,但是因为某些原因导致不能访问。
首先报错的原因是因为存在暂时性死区,我们不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。然后这里你认为的提升和 var 的提升是有区别的,虽然变量在编译的环节中被告知在这块作用域中可以访问,但是访问是受限制的。
那么到这里,想必大家也都明白 var、let 及 const 区别了,不知道你是否会有这么一个疑问,为什么要存在提升这个事情呢,其实提升存在的根本原因就是为了解决函数间互相调用的情况
function test1() { test2()}function test2() { test1()}test1()
假如不存在提升这个情况,那么就实现不了上述的代码,因为不可能存在 test1 在 test2 前面然后 test2 又在 test1 前面。
那么最后我们总结下这小节的内容:
- 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
- var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
- var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
- let 和 const 作用基本一致,但是后者声明的变量不能再次赋值 原型继承和 Class 继承
涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来讲下 class,其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。
class Person {}Person instanceof Function // true
在上一章节中我们讲解了原型的知识点,在这一小节中我们将会使用分别使用原型和 class 的方式来实现继承。
既然要实现继承,那么首先我们得有一个父类,代码如下:
// 定义一个动物类function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉!'); }}// 原型方法Animal.prototype.eat = function(food) { console.log(this.name + '正在吃:' + food);};
1、原型链继承
核心: 将父类的实例作为子类的原型
function Cat(){ }Cat.prototype = new Animal();Cat.prototype.name = 'cat';// Test Codevar cat = new Cat();console.log(cat.name);console.log(cat.eat('fish'));console.log(cat.sleep());console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
- 无法实现多继承
- 来自原型对象的所有属性被所有实例共享(
来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1) - 创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
2017-8-17 10:21:43补充:感谢 MMHS 指出。缺点1中描述有误:可以在Cat构造函数中,为Cat实例增加实例属性。如果要新增原型属性和方法,则必须放在 new Animal() 这样的语句之后执行。
2018-9-10 00:03:45补充:感谢 IRVING_J 指出。缺点3中的描述不够充分。更正为:来自原型对象的所有属性被所有实例共享。
2、构造继承
核心: 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){ Animal.call(this); this.name = name || 'Tom';}// Test Codevar cat = new Cat();console.log(cat.name);console.log(cat.sleep());console.log(cat instanceof Animal); // falseconsole.log(cat instanceof Cat); // true
特点:
- 解决了1中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3) 3、实例继承
核心: 为父类实例添加新特性,作为子类实例返回
function Cat(name){ var instance = new Animal(); instance.name = name || 'Tom'; return instance;}// Test Codevar cat = new Cat();console.log(cat.name);console.log(cat.sleep());console.log(cat instanceof Animal); // trueconsole.log(cat instanceof Cat); // false
特点:
- 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
- 实例是父类的实例,不是子类的实例
- 不支持多继承
推荐指数:★★
4、拷贝继承
function Cat(name){ var animal = new Animal(); for(var p in animal){ Cat.prototype[p] = animal[p]; }
****// 2020年10月10日21点36分:感谢 @baclt 的指出,如下实现修改了原型对象,会导致单个实例修改name,会影响所有实例的name值 ****// Cat.prototype.name = name || 'Tom'; 错误的语句,下一句为正确的实现
this.name = name || 'Tom';}// Test Codevar cat = new Cat();console.log(cat.name);console.log(cat.sleep());console.log(cat instanceof Animal); // falseconsole.log(cat instanceof Cat); // true
特点:
- 支持多继承
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
组合继承
组合继承是最常用的继承方式,
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = new Parent()const child = new Child(1) child.getValue() // 1child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) { this.val = value}Parent.prototype.getValue = function() { console.log(this.val)}function Child(value) { Parent.call(this, value)}Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true }})const child = new Child(1) child.getValue() // 1child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单
class Parent { constructor(value) { this.val = value } getValue() { console.log(this.val) }}class Child extends Parent { constructor(value) { super(value) this.val = value }}let child = new Child(1)child.getValue() // 1child instanceof Parent // true
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。
当然了,之前也说了在 JS 中并不存在类,class 的本质就是函数。