-
Notifications
You must be signed in to change notification settings - Fork 0
JavaScript知识点整理
本章节对应demo练习 在学习继承的相关知识点之前,先整理一下创建对象的几种模式以及它们的优缺点
背景:最初是使用Object构造函数和单个字面量创建单个对象。 缺点:使用同一个接口创建多个对象,会产生大量重复的代码。 为了解决这个问题,使用了工厂模式的一种变体。
考虑到ECMAScript中无法创建类,就使用函数来封装以特定接口创建对象的细节。
function createPerson(name, age, job) {
var o = Object.create(null)
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log(this.name)
}
return o
}
var person1 = createPerson('test1', '18', 'student')
var person2 = createPerson('nina', '2*', 'web developer')
解决了上述问题,无论调用多少次,只返回包含三个属性的对象;缺点:创建的对象原型指向null,无法识别对象。
function Person() {
this.name = 'Tylor'
this.songs = ['You Belong With me']
this.sayName = function () {
console.log(this.name)
}
}
function Child() {
Person.call(this)
}
var child0 = new Child()
console.log(Object.getPrototypeOf(child0) === Child.prototype) // true
var child1 = new Child()
child1.songs.push('What')
var child2 = new Child()
console.log(child2.songs) // ['You Belong With me']
使用构造函数创建实例,必须用new
操作符,意味着可以将它的实例标识为一种特定的类型。缺点:每个方法都要在每个实例上创建一遍,若将函数提到全局,可解决这个问题,但又会污染全局作用域。因为该函数实际只能被某个对象调用。
function Person() {
}
var friend = new Person()
Person.prototype.sayHi = function () {
console.log('hello')
}
Person.prototype = {
// constructor: Person,
name: 'prototype',
friends: ['a', 'b'],
sayName: function () {
return this.name
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('c')
console.log(person1.sayName === person2.sayName) // true
console.log(person1.friends === person2.friends) // true
console.log(person1.constructor == Object) // true
console.log(friend.constructor == Person) // true
console.log(friend.sayHi()) // 'hello'
console.log(friend.sayName) // undefined
特点:所有对象实例共享它所包含的属性和方法,这即是原型模式的优点也是它的缺点,对于引用类型的属性来说,这个问题比较突出。
需要注意的是:如果设置构造函数的prototype
为对象字面量形式创建的新对象,那么constructor
将不再指向该构造函数,并且会切断了现有原型与任何之前已存在的对象实例之间的关系。
开始继承前,还要再捋捋这三个到底是啥。
只要创建了一个新函数,就会(根据一组特定的规则)为该函数创建一个prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有的原型都会自动获得一个contructor
属性,这个属性包含一个指向prototype
属性所在函数的指针。当调用构造函数创建一个新实例,新实例中会包含一个(__proto__
)指向构造函数的原型对象。
再总结一下:prototype
是(构造)函数的一个属性,指向函数的原型对象;constructor
是原型对象默认属性,指向(构造)函数;__proto__
是构造函数实例的属性,指向构造函数的原型对象。
假设创建了一个Person
构造函数,如果将Person.prototype
设置为等于一个以字面量创建的对象,那么constructor
就不再指向Person
了,而是指向Object
(也就是等于以字面量创建对象的constructor
了,重新赋值了嘛。);如果constructor
很重要的话,可以重新设置其值为Person
。重写原型对象还会切断现有原型与之前已存在实例之间的联系;已存在实例引用的仍然是最初的原型。
许多OO语言支持两种继承方式:接口继承和实现继承。ECMAscript只支持实现继承,其实现继承主要是依靠原型链来实现。
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.property = '111'
this.color = ['violet', 'orange']
}
function SubType() {
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.property // '111'
// 但是问题如下:
instance1.color.push('red')
var instance2 = new SubType()
instance2.color // ['violet', 'orange', 'red']
缺点:1.包含引用类型的原型,原型属性会被所有实例共享;2.实例不能向超类传参,也就是说不能在不影响所有对象实例的情况下,给超类型的构造函数传参。
function SuperType(name) {
this.name = name
this.color = ['violet', 'orange']
}
function SubType(name) {
SuperType.call(this, name)
}
var instance1 = new SubType()
instance1.color.push('white')
var instance2 = new SubType(passenger)
instance2.color // ['violet', 'orange']
//可传参
instance2.name //'passenger'
// 问题是:
SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
console.log(this.name)
}
var instance3 = new SubType()
instance3.age //undefined
instance3.sayName //undefined
优点:每个实例都会具有自己的属性;实例可向超类型传参。
缺点:实例的方法无法复用;超类型的原型中的属性和方法,子类型无法继承。
function SuperType(name) {
this.name = name
this.color = ['violet', 'orange']
}
SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, job) {
SuperType.call(this, name) // 第二次调用
this.job = job
}
SubType.prototype = new SuperType() // 第一次调用
// 组合继承的问题(创建了不必要的属性,通过构造函数覆盖该属性)
console.log(SubType.prototype.color) // [ 'violet', 'orange' ]
SubType.prototype.constructor = SubType
SubType.prototype.sayJob = function() {
console.log(this.job)
}
var instance1 = new SubType('test1', 'student')
instance1.color.push('blue')
instance1.sayName() // 'test1'
var instance2 = new SubType('nina', 'web developer')
instance2.color //['violet', 'orange']
instance2.sayJob() //'web developer'
组合继承避免了原型链和借用构造函数的缺点,融合了两者的优点,成为Javascript中最常用的继承模式。
缺点:无论什么情况,都需要调用两次超类构造函数;不得不在调用子类型构造函数中重写对象实例的属性。
通过借用构造函数继承属性,通过原型链的混成形式继承方法。解决了组合继承的问题。
function SuperType(name) {
this.name = name
this.color = ['violet', 'orange']
}
SuperType.prototype.age = '18'
SuperType.prototype.sayName = function() {
return this.name
}
function SubType(name, job) {
SuperType.call(this, name)
this.job = job
}
// 留意
function F() {}
F.prototype = SuperType.prototype;
SubType.prototype = new F()
SubType.prototype.constructor = SubType
SubType.prototype.sayJob = function() {
return this.job
}
该方式只调用了一次SuperType
构造函数,并且因此避免了在SubType.prototype
上创建不必要的、多余的属性。所以普片认为寄生组合式继承是最理性的继承范式。
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类实际上是个“特殊的函数”,类语法有两个组成部分:类表达式和类声明。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x},${this.y})`;
}
}
- 其中
constructor
就是构造方法,this
关键字代表实例对象。 - 类的所有方法都是定义在类的
prototype
属性上的。
// 等同于
Point.prototype = {
constructor(){},
toString() {}
}
- 但是类内部定义的所有方法都是不可枚举的。(这一点与ES5)行为不一致。
Object.keys(Point.prototype); // []
一个类必须有constructor
方法,若是没有显示定义,一个空的constructor
方法会被默认添加。constructor
方法默认返回实例对象this
,也可以指定返回其他对象。若果返回新对象,那么实例的对象将不是(Foo
)类的实例。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo; // false
类必须使用new
调用,否则会报错。
与ES5写法一致,并且都是设置在属性的描述符对象上的。
class Point {
constructor() {
}
set prop(val) {
console.log('setter:' + val);
}
get prop() {
console.log('getter');
}
}
const instance = new Point();
instance.prop = 123; // setter:123
instance.prop; // getter
const descriptor = Object.getOwnPropertyDescriptor(Point.prototype, 'prop');
console.log('get' in descriptor);
console.log('set' in descriptor);
- 类和模块内部默认就是严格模式;
- 不存在提升。(为了保证在继承的时候,子类在父类之后定义。)
- name属性,本质上ES6的类知识ES5构造函数的一层包装,所以函数的许多特性都被
class
继承,包括name
。 - Generator方法,如果在某个方法的前面加上(
*
) ,就表示该方法是Generator方法。 - this的指向,类的方法内部如果包含
this
, 它默认指向类的实例。如果要单独使用内部有this
的方法,则需手动将this
绑定到类的实例上,否则很可能报错。因为类内部使用的是严格模式,所以this
实际上指向的是undefined
。
如果一个方法前加上了static
关键字,则该方法不会被实例继承,而是直接通过类调用。静态方法内部的this
指向类而不是实例。父类的静态方法,可以被子类继承。并且静态方法也是可以从super
对象上调用。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar();// hello
实例属性可以直接定义在类的最顶层。
class Foo {
_count = 0;
...
}
new.target
返回new
作用于的那个构造函数的名称。需要注意的是子类继承父类时,new.target
返回的是子类的名称。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
Class通过extends
关键字,继承父类所有的属性和方法。子类constructor
中需调用super
方法,否则在创建实例的时候会抛错。如果子类不调用super
方法,就得不到this
对象。
与ES5不同, ES6继承机制是先将父类的实例对象的属性和方法,添加到this
上(所以必须先调用super
方法),然后用子类的构造函数修改this
。
只有super
可以调用父类实例。在子类构造函数中,只有调用了super
之后,才可以使用this
关键字。
super
既可以当作函数使用,又可以当作对象使用。
-
super
作为函数调用时,代表父类的构造函数。但返回的是子类的实例。 -
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 -
super()
作为函数时,只能用在子类的构造函数之中。 -
由于
super
在普通方法中指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。 -
普通方法通过
super
调用父类方法时,方法内部的this
指向当前子类的实例。 -
super
在静态方法中指向父类,但通过super
调用父类方法时,方法内部的this
指向当前的子类。 而不是子类的实例。 -
注意
super
必须显示指定作为函数或者对象使用,否则会报错。
class A {
constructor() {
this.x = 1;
this.test = 'test';
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
console.log(super.test);
super.print();
}
}
B.x = 3;
B.m();
// undefined
// 3
Promise
是异步编程的一种解决方案,比传统的解决方案——回调和事件——更合理和强大。Promise
总共有三种状态:pending
、fulfilled
、rejected
;只有异步操作的结果可以决定当前处于那种状态,其余手段无法改变。状态只会由pending
转变为fulfilled
或者由pending
转变为rejected
,状态一旦转变就不会再变了,并且会一直保持这个结果,这是就成为resolved(已定型)。
Promise
的缺点:
- 无法取消,一旦新建就会立即执行,无法中途取消;
- 如果不设置回调,promise内部抛的错误不会反应到外部
- 当处于
pending
时,无法得知目前进行到哪个阶段
ES6规定,Promise
是一个构造函数,用来生产Promise
实例。