对象原型链及es6相关汇总

# 对象

# JavaScript有哪些内置的对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。

标准内置对象的分类:

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。 例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。 例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。 例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。 例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。 例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。 例如 JSON 等

(10)控制抽象对象 例如 Promise、Generator 等

(11)反射 例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。 例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他 例如 arguments

总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

# 浅拷贝

只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存

# 实现方式

  • Object.assign():需注意的是目标对象只有一层的时候,是深拷贝
  • Array.prototype.concat()
  • Array.prototype.slice()
  • 扩展运算符spread(...):转换数组为用逗号分隔的参数序列([...arr]spread相当于rest参数的逆运算); ES6 的合并数组, [...arr1, ...arr2];

# 深拷贝

就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系

深拷贝的做法一般分两种:

  • JSON.parse(JSON.stringify(a)) 简单深遍历;但是这种拷贝方法不可以拷贝一些特殊的属性(例如正则表达式,undefine,function)
  • 递归浅拷贝

第一种做法存在一些局限,很多情况下并不能使用;第二种做法一般是工具库中的深拷贝函数实现方式,比如 loadash 中的 cloneDeep

# 实现方式

  • 热门的函数库lodash,也有提供**_.cloneDeep用来做深拷贝**
  • jquery 提供一个$.extend可以用来做深拷贝, 这个也有深浅拷贝之分(false,true);
  • JSON.parse(JSON.stringify())。简单数据类型;记得这个特殊
  • 手写递归拷贝

# 所有对象的深度克隆

(包装对象,Date对象,正则对象)

通过递归可以简单实现对象的深度克隆,但是这种方法不管是ES6还是ES5实现,都有同样的缺陷,就是只能实现特定的object的深度复制(比如数组和函数),不能实现包装对象Number,String , Boolean,以及Date对象,RegExp对象的复制。

# (1)前文的方法
function deepClone(obj){
    var newObj= obj instanceof Array?[]:{};
    for(var i in obj){
       newObj[i]=typeof obj[i]=='object'?  
       deepClone(obj[i]):obj[i];    
    }
    return newObj;
}
1
2
3
4
5
6
7
8

这种方法可以实现一般对象和数组对象的克隆,比如:

var arr=[1,2,3];
var newArr=deepClone(arr);
// newArr->[1,2,3]

var obj={
   x:1,
   y:2
}
var newObj=deepClone(obj);
// newObj={x:1,y:2}
1
2
3
4
5
6
7
8
9
10

但是不能实现例如包装对象Number,String,Boolean,以及正则对象RegExp和Date对象的克隆,比如:

//Number包装对象
var num=new Number(1);
typeof num // "object"

var newNum=deepClone(num);
//newNum ->  {} 空对象

//String包装对象
var str=new String("hello");
typeof str //"object"

var newStr=deepClone(str);
//newStr->  {0:'h',1:'e',2:'l',3:'l',4:'o'};

//Boolean包装对象
var bol=new Boolean(true);
typeof bol //"object"

var newBol=deepClone(bol);
// newBol ->{} 空对象

....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# (2)valueof()函数

所有对象都有valueOf方法,valueOf方法对于:如果存在任意原始值,它就默认将对象转换为表示它的原始值。对象是复合值,而且大多数对象无法真正表示为一个原始值,因此默认的valueOf()方法简单地返回对象本身,而不是返回一个原始值。数组、函数和正则表达式简单地继承了这个默认方法,调用这些类型的实例的valueOf()方法只是简单返回这个对象本身。

对于原始值或者包装类:

function baseClone(base){
 return base.valueOf();
}

//Number
var num=new Number(1);
var newNum=baseClone(num);
//newNum->1

//String
var str=new String('hello');
var newStr=baseClone(str);
// newStr->"hello"

//Boolean
var bol=new Boolean(true);
var newBol=baseClone(bol);
//newBol-> true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

其实对于包装类,完全可以用=号来进行克隆,其实没有深度克隆一说,

这里用valueOf实现,语法上比较符合规范。

对于Date类型:

因为valueOf方法,日期类定义的valueOf()方法会返回它的一个内部表示:1970年1月1日以来的毫秒数.因此我们可以在Date的原型上定义克隆的方法:

Date.prototype.clone=function(){
  return new Date(this.valueOf());
}

var date=new Date('2010');
var newDate=date.clone();
// newDate->  Fri Jan 01 2010 08:00:00 GMT+0800 
1
2
3
4
5
6
7

对于正则对象RegExp:

RegExp.prototype.clone = function() {
var pattern = this.valueOf();
var flags = '';
flags += pattern.global ? 'g' : '';
flags += pattern.ignoreCase ? 'i' : '';
flags += pattern.multiline ? 'm' : '';
return new RegExp(pattern.source, flags);
};

var reg=new RegExp('/111/');
var newReg=reg.clone();
//newReg->  /\/111\//
1
2
3
4
5
6
7
8
9
10
11
12

# new/Object.create

# new操作符的实现步骤

  • 创建一个对象;
  • 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性);
  • 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法);
  • 返回新的对象;
var new2 = function (func) {
    var o = Object.create(func.prototype);//创建对象
    var oc = func.call(o);//改变this指向,把结果付给oc
    if (oc && oc instanceof Object) {//判断oc的类型是不是对象
        return oc;//是,返回oc
    } else {
        return o;//不是, 返回构造函数的执行结果
    }
}  
1
2
3
4
5
6
7
8
9

# new操作符的实现原理

new操作符的执行过程:

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

function objectFactory() {
    let newObject = null,
        constructor = Array.prototype.shift.call(arguments),
        result = null;
    // 参数判断
    if (typeof constructor !== "function") {
        console.error("type error");
        return;
    }
    // 新建一个空对象,对象的原型为构造函数的 prototype 对象
    newObject = Object.create(constructor.prototype);
    // 将 this 指向新建对象,并执行函数
    result = constructor.apply(newObject, arguments);
    // 判断返回对象
    let flag = result && (typeof result === "object" || typeof result === "function");
    // 判断返回结果
    return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Object.create 和 new 区别【要点】

  • Object.create 是创建一个新对象,使用现有的对象来提供新创建对象的 proto。意思就是生成一个新对象,该新对象的 proto(原型) 指向现有对象

  • new 生成的是构造函数的一个实例,实例继承了构造函数及其 prototype(原型属性)上的属性和方法。 new比上面Object.create的实现,多了一步call的实现;

# Object.create原理解析实现

x.__proto__ 等同于 Object.setPrototypeOf(x, P.prototype)

Object.create = function (obj) {
  //return { '__proto__': obj};//方式一:简洁;
  const target = {} //方式二;
  target.__proto__ = obj
  return target
};
//Object.setPrototypeOf(obj, P.prototype) // 将对象与构造函数原型链接起来
//obj.__proto__ = P.prototype // 等价于上面的写法
1
2
3
4
5
6
7
8

# new原理解析实现

  1. 创建一个空对象
  2. 将对象的_proto_指向构造函数的 prototype
  3. 将这个构造函数作为这个对象的this
  4. 返回该对象
var o = Object.create(func.prototype);
var oc = func.call(o);
//或
var o  = {};//创建一个空对象
o.__proto__ = Base.prototype;//使用现有的对象来提供新创建对象的 _proto_
var oc = Base.call(obj);//继承原对象属性和方法
return oc instanceof Object ? oc : o
1
2
3
4
5
6
7

示例:以下示例等同; 直接变成立即执行的函数;

// let p = new Person()//下面等同于;
let p = (function () {
    let obj = {};
    obj.__proto__ = Person.prototype;
    obj = Person.call(obj)
    return obj;
})();

function myNew(Con,...args) {
    let obj = Object.create(Con.prototype);
    let result = obj.apply(obj,args);
    return typeof result === 'object' ? result : obj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 原型与原型链

# 对原型、原型链的理解

在JavaScript中使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

# __proto__和prototype的区别和关系【要点】

__proto__(隐式原型)与prototype(显式原型)

隐式原型:JavaScript中任意对象都有一个内置属性[[prototype]],在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__来访问。ES5中有了对于这个内置属性标准的Get方法Object.getPrototypeOf() 【 Object.prototype 这个对象是个例外,它的__proto__值为null】

显式原型:每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。

# 二者的关系

隐式原型指向创建这个对象的函数(constructor)的prototype

# 作用
  • 隐式原型的作用:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找。
  • 显式原型的作用:用来实现基于原型的继承与属性的共享

# 原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true
1
2
3
4
5
6
7

# 原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person
1
2
3
4
5
6
7

# 原型链的终点是什么?如何打印出原型链的终点?

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true

所以,原型链的终点是null 原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__在这里插入图片描述

# 如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
    var res=[];
    for(var key in obj){
        if(obj.hasOwnProperty(key))
            res.push(key+': '+obj[key]);
    }
    return res;
} 
1
2
3
4
5
6
7
8
function Person() {}
//Person.prototype = {
//    name: 'samy'
//}
Person.prototype.name = 'samy'
var p = new Person();
p.age = 10;

p.hasOwnProperty('age') // true
p.hasOwnProperty('name') // false
1
2
3
4
5
6
7
8
9
10

# Function._proto_(getPrototypeOf)是什么?

获取一个对象的原型,在chrome中可以通过__proto__的形式,或者在ES6中可以通过Object.getPrototypeOf的形式。

那么Function.proto是什么么?也就是说Function由什么对象继承而来,我们来做如下判别。

Function.__proto__==Object.prototype //false
Function.__proto__==Function.prototype//true
1
2

我们发现Function的原型也是Function。

我们用图可以来明确这个关系:

2018-07-10 2 38 27

# 下面代码的输出结果

function Person(name) {
    this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 下面代码的输出结果

function Dog() {
    this.name = 'samy'
}
Dog.prototype.bark = () => {
    console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
1
2
3
4
5
6
7
8

打印出:true

因为constructor是prototype上的属性,所以dog.constructor实际上就是指向Dog.prototype.constructor;constructor属性指向构造函数。instanceof而实际检测的是类型是否在实例的原型链上。

constructor是prototype上的属性,这一点很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地来说,constructor的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而instanceof比较松散,只要检测的类型在原型链上,就会返回true。

# 下面代码的输出结果

function Dog() {
    this.name = 'samy'
}
Dog.prototype.bark = () => {
    console.log('woof!woof!')
}
function BigDog() {}
BigDog.prototype = new Dog()
const bigDog = new BigDog()
console.log(bigDog.constructor === BigDog)
1
2
3
4
5
6
7
8
9
10

打印出:false

因为bigDog的原型是Dog的实例,所以访问bigDog.constructor时实际访问的是Dog.prototype.constructor,也就是Dog。所以 bigDog.constructor === Dog,这样才会打印出true。

这个例子证明了单纯用constructor来判断实例的类型是有隐患的,因为bigDog.constructor === BigDog是false,就说明无法用constructor来准确判断bigDog一个实例的类型。

可以这样修改:

function Dog() {
    this.name = 'puppy'
}
Dog.prototype.bark = () => {
    console.log('woof!woof!')
}
function BigDog() {}
BigDog.prototype = new Dog()
// 修复constructor的指向
Object.defineProperty(BigDog.prototype, "constructor", {
    value: BigDog,
    enumerable: false,
})
const bigDog = new BigDog()
console.log(bigDog.constructor === BigDog)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这段代码通过显式地更改BigDog.prototype的指向来修正上面提到的隐患,那为什么要这么大费周章地使用Object.defineProperty,而不是直接这样做呢:

BigDog.prototype.constructor = BigDog
1

是因为不希望 constructor 这个属性被 for…in 遍历到,所以用 defineProperty 来控制访问权限。

for(let i in bigDog){console.log(i)} // 会出现constructor属性
1

如果用上面两个问题去测试,就会发现ES6中的class本质上只是原型链的语法糖。

class Cat {
    constructor(props) {
      this.name = 'puppy'
    }
  }
class BigCat extends Cat {
    constructor(props) {
      super(props)
    }
}
const bigCat = new BigCat()
console.log(bigCat.constructor === BigCat) //true
for(let i in bigCat){
    console.log(i)// 不会出现constructor属性
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# ES6相关

# class

# 继承的方式【要点】

JS是一门弱类型动态语言,封装和继承是他的两大特性;

常用的六种继承方式

方式一:【原型链继承

//因为会改变原型的指向,所以应该放到重新指定之后
Student.prototype = new Person()//注意这里的顺序;
Student.prototype.sayHello = function () { }
1
2
3

方式二:【构造继承】call继承借用构造函数继承://使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Student(name, age, price) {
  Person.call(this, name, age)// 相当于: this.Person(name, age)
  this.price = price
}
1
2
3
4

方式三:【实例继承】【不常用】;核心:为父类实例添加新特性,作为子类实例返回

方式四: 【拷贝继承】【不常用】冒充对象继承

方式五:【组合继承】原型链+借用构造函数的组合继承;

function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
1
2
3
4
5
6
7

寄生式组合: call继承+Object.create();

  • 与此同时原型链还能保持不变,所以可以正常使用instanceof 和 isPrototypeOf() ,所以寄生组合继承是引用类型最理想的继承方法。
  • 核心部分:三步走:1:修改this指向到孩子;2:设置父的属性构造为孩子;3:设置父的属性为孩子属性;
// 不会初始化两次实例方法/属性,避免的组合继承的缺点
//Student.prototype = Person.prototype//要点
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student//要点

inheritPrototype(Student,Person)
function inheritPrototype(child,parent){
  // var prototype = object(parent.prototype);
  var pPrototype = Object.create(parent.prototype);//创建对象
  pPrototype.constructor = child;//增强对象;修改指向;
  child.prototype = pPrototype;//指定对象
}
1
2
3
4
5
6
7
8
9
10
11
12

方式六: ES6 class继承

  • ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
  • ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

# 类的创建和继承

(1)类的创建(es5):new一个function,在这个function的prototype里面增加属性和方法。

下面来创建一个Animal类:

// 定义一个动物类
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
2
3
4
5
6
7
8
9
10
11
12
13

这样就生成了一个Animal类,实力化生成对象后,有方法和属性。

(2)类的继承——原型链继承

function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
// Test Code
var 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
1
2
3
4
5
6
7
8
9
10
  • 介绍:在这里我们可以看到new了一个空对象,这个空对象指向Animal并且Cat.prototype指向了这个空对象,这种就是基于原型链的继承。
  • 特点:基于原型链,既是父类的实例,也是子类的实例
  • 缺点:无法实现多继承

(3)构造继承:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
1
2
3
4
5
6
7
8
9
10
  • 特点:可以实现多继承
  • 缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。

(4)实例继承和拷贝继承

实例继承:为父类实例添加新特性,作为子类实例返回

拷贝继承:拷贝父类元素上的属性和方法

上述两个实用性不强,不一一举例。

(5)组合继承:相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
1
2
3
4
5
6
7
8
9
10
11
12
  • 特点:可以继承实例属性/方法,也可以继承原型属性/方法
  • 缺点:调用了两次父类构造函数,生成了两份实例

(6)寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();
// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 较为推荐

# 继承实质

无非就是是否先创建子类this,或者将父类的属性方法添加到this上;

  • ES5实质:先创造子类实例的this,再将父类的属性方法添加到this上(继承赋值处理)
  • ES6实质:先将父类实例的属性方法加到this上(调用super()),再用子类构造函数修改this

# Class 和传统构造函数比较

  • Class 在语法上更加贴合面向对象的写法
  • Class 实现继承更加易读、易理解,对初学者更加友好
  • 本质还是语法糖,使用prototype
  • 用新语法调用父原型方法的版本比旧语法要简单得多,用super.method()代替Parent.prototype.method.call(this)Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)

类的数据类型就是function,类本身就指向构造函数。构造函数的prototype属性,在ES6的“类”上面继续存在

# 面向对象的三大特性

# TS

# proxy

# Object.defineProterty

ES5出来的方法; 三个参数: 对象(必填), 属性值(必填), 描述符(可选);

defineProterty的描述符属性

数据属性: value, writable, configurable, enumerable 访问器属性: get, set 注:不能同时设置value和writable,这两对属性是互斥的

# defineProterty和proxy的对比

1.defineProterty是es5的标准,proxy是es6的标准;

2.proxy可以监听到数组索引赋值,改变数组长度的变化;

3.proxy是监听对象,不用深层遍历,defineProterty是监听属性;

4.利用defineProterty实现双向数据绑定(vue2.x采用的核心)

5.利用proxy实现双向数据绑定(vue3.x会采用)

Proxy 的优势如下:

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

Object.defineProperty 的优势如下:

  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

# 扩展运算符

# 扩展运算符的作用及使用场景

(1)对象扩展运算符 对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }
1
2

上述方法实际上等价于:

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
1
2

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。 同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}
1
2

利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。

这里有点需要注意的是扩展运算符对对象实例的拷贝属于一种浅拷贝(2)数组扩展运算符 数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5
1
2
3
4

下面是数组的扩展运算符的应用:

  • 将数组转换为参数序列
function add(x, y) {
  return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
1
2
3
4
5
  • 复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];
1
2

要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

  • 合并数组

如果想在数组内合并数组,可以这样:

const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
1
2
3
  • 扩展运算符与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]
1
2
3

需要注意:

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const [...rest, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...rest, last] = [1, 2, 3, 4, 5];
// 报错
1
2
3
4
  • 将字符串转为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
1
2
  • 任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组

比较常见的应用是可以将某些数据结构转为数组,比如:

// arguments对象
function foo() {
  const args = [...arguments];
}
1
2
3
4

用于替换es5中的Array.prototype.slice.call(arguments)写法。

  • 使用Math函数
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9
1
2
3

# Map

# 方法

  • get():返回键值对
  • set():添加键值对,返回实例
  • delete():删除键值对,返回布尔值
  • has():检查键值对,返回布尔值
  • clear():清除所有成员
  • keys():返回以键为遍历器的对象
  • values():返回以值为遍历器的对象
  • entries():返回以键和值为遍历器的对象
  • forEach():使用回调函数遍历每个成员

# 重点难点

  • 遍历顺序:插入顺序
  • 对同一个键多次赋值,后面的值将覆盖前面的值
  • 对同一个对象的引用,被视为一个键
  • 对同样值的两个实例,被视为两个键
  • 键跟内存地址绑定,只要内存地址不一样就视为两个键
  • 添加多个以NaN作为键时,只会存在一个以NaN作为键的值
  • Object结构提供字符串—值的对应,Map结构提供值—值的对应

# map和Object的区别

Map Object
意外的键 Map默认情况不包含任何键。只包含显式插入的键。 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
键的类型 Map的键可以是任意值,包括函数、对象或任意基本类型。 Object 的键必须是 String 或是Symbol。
键的顺序 Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 Object 的键是无序的
Size Map 的键值对个数可以轻易地通过size 属性获取 Object 的键值对个数只能手动计算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代Object需要以某种方式获取它的键然后才能迭代。
性能 在频繁增删键值对的场景下表现更好。 在频繁添加和删除键值对的场景下未作出优化。

# map和weakMap的区别

(1)Map

map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。 如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。 实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

const map = [
     ["name","张三"],
     ["age",18],
]
1
2
3
4

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear():map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
const map = new Map([
     ["foo",1],
     ["bar",2],
])
for(let key of map.keys()){
    console.log(key);  // foo bar
}
for(let value of map.values()){
     console.log(value); // 1 2
}
for(let items of map.entries()){
    console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach( (value,key,map) => {
     console.log(key,value); // foo 1    bar 2
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

(2)WeakMap

WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

该对象也有以下几种方法:

  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

WeakMap 允许垃圾收集器执行其回收任务,但Map不允许。对于手动编写的 Map,数组将保留对键对象的引用,以防止被垃圾回收。但在WeakMap中,对键对象的引用被“弱”保留,这意味着在没有其他对象引用的情况下,它们不会阻止垃圾回收。

var map = new Map()
var weakMap = new WeakMap()

var a = {
  x: 12
};
var b = {
  y: 12
};
map.set(a, 1);
weakMap.set(b, 2);
console.log(map);//Map { { x: 12 } => 1 }
console.log(weakMap);//WeakMap { [items unknown] }

console.log(map.get(a));//1
console.log(weakMap.get(b));//2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Set

方法

  • add():添加值,返回实例
  • delete():删除值,返回布尔值
  • has():检查值,返回布尔值
  • clear():清除所有成员
  • keys():返回以属性值为遍历器的对象
  • values():返回以属性值为遍历器的对象
  • entries():返回以属性值和属性值为遍历器的对象
  • forEach():使用回调函数遍历每个成员

# 应用场景

  • 去重字符串[...new Set(str)].join("")
  • 去重数组[...new Set(arr)]Array.from(new Set(arr))
  • 集合数组
    • 声明:const a = new Set(arr1)const b = new Set(arr2)
    • 并集new Set([...a, ...b])
    • 交集new Set([...a].filter(v => b.has(v)))
    • 差集new Set([...a].filter(v => !b.has(v)))

# 模块导入出

# JS之AMD、CMD、CommonJS、ES6、UMD的使用

# 图示对比

image-20200820091303580

# 总括【ACCE】
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
  • CommonJS规范主要用于服务端编程**,加载模块是同步的**,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案

# 面向对象

# 1. 对象创建的方式有哪些?

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:

(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。

(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。

(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。

(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

# 2. 对象继承的方式有哪些?

(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。

(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。

(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

# 3. 如何判断一个对象是否属于某个类?

  • 第一种方式是使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
  • 第二种方式可以通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
  • 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。
上次更新: 2022/04/15, 05:41:33
×