3

面向对象

js是一门基于对象的语言。js中的一切皆对象;

console.log(Object.prototype.toString.call(123)) //[object Number]        
console.log(Object.prototype.toString.call('123')) //[object String]      
console.log(Object.prototype.toString.call(undefined)) //[object Undefined] 
console.log(Object.prototype.toString.call(true)) //[object Boolean]      
console.log(Object.prototype.toString.call({})) //[object Object]        
console.log(Object.prototype.toString.call([])) //[object Array]      
console.log(Object.prototype.toString.call(function(){})) //[object Function]


new function


    var a = function () {};
    console.log(typeof a);//function
    
    var b = new function () {};
    console.log(typeof b);//object
    
    var c = new Function ();
    console.log(typeof c);//function
    
new function 是一个JavaScript中用户自定义的对象

    var obj = function (name) {
        this.name = name;
    };
    var b = new obj('aaa')=o == Object.create(obj.prototype);;
    console.log(b.name);  
    // 创建一个以另一个空对象为原型,且拥有一个属性p的对象
    o = Object.create({}, { p: { value: 42 } })
    
    // 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的:
    o.p = 24
    o.p
    //42

私有变量和函数
在函数内部定义的变量和函数,叫局部(内部)变量和函数,如果不对外提供接口,外部是无法访问到的。

function Box(){
    var color = "blue"; //私有变量
    var fn = function(){} //私有函数
}
var obj = new Box();
    alert(obj.color); //弹出 undefined,访问不到私有变量
    alert(obj.fn); //同上

静态变量和函数
定义一个函数后加"."来添加的属性和函数,该函数可以访问到,但实例访问不到。

function Obj(){};

Obj.num = 72; //静态变量
Obj.fn = function() { } //静态函数
  
alert(Obj.num); //72
alert(typeof Obj.fn) //function

var t = new Obj();
alert(t.name); //undefined
alert(typeof t.fn); //undefined

实例变量和函数

function Box(){
    this.a=[]; //实例变量
    this.fn=function(){} //实例方法
}

var box1=new Box();
box1.a.push(1);
box1.fn={};
console.log(box1.a); //[1]
console.log(typeof box1.fn); //object

var box2=new Box();
console.log(box2.a); //[]
console.log(typeof box2.fn); //function

box1中修改了afn,而在box2中没有改变,由于数组和函数都是对象,是引用类型,这就说明box1中的属性和方法与box2中的属性与方法虽然同名但却不是一个引用,而是对Box对象定义的属性和方法的一个复制。

ES5

构造函数

构造函数(constructor),其实就是一个普通函数,但是内部使用了this变量,对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上

function Cat(name,color){
    this.name=name;
    this.color=color;
}
Cat.prototype.type=function(){};

var cat1=new Cat()

这时cat1会自动含有一个constructor属性,指向它们的构造函数。

alert(cat1.constructor==Cat);//true

alert(cat instanceof Cat);//true

js提供了一个instanceof运算符,用来检验cat1是否是Cat的实例对象。

原型对象与实例对象之间的关系。

构造函数(constructor):每new生成一个实例,就相当于在内存上又复制了一次
原型对象(prototype):而portotype,所有的实例都只指向一个内存地址,用于不变的属性和方法

不管是构造函数内部还是原型对象,里面的this在没有new之前都指向该构造函数Cat

Cat.prototype.constructor===Cat //true
alert(Cat.prototype.isPrototypeof(cat1)) //true

isPrototypeOf()用来判断某个prototype对象和某个实例之间的关系

alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false 

hasOwnProperty()用来判断某个属性到底是本地属性,还是继承自prototype对象的属性。本地为true

in运算符用来判断,某个实例是否含有某个属性,不管是不是本地属性。还可以用来遍历某个对象的所有属性。

alert("name" in cat1);//true
for(var i in cat1){alert("cat1["+i+"]="+cat1[i])}

继承

构造函数绑定

    function Cat(name,color){

    Animal.apply(this, arguments);//等于是把父类的实例属性复制了一份给子类实例装上了,占内存

    this.name = name;
    this.color = color;

  }

  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

利用prototype

    Cat.prototype = new Animal(); //由于prototype 引用类型指向同一个地址会影响其它实例,而且不能向父传参
  Cat.prototype.constructor = Cat; //把prototype指向原来的构造函数

组合继承

    function Cat(){
        Animal.call(this);
    }
    Cat.prototype = new Animal(); //比较常用,占内存
 

通过空对象


    function extend(Child, Parent) {
    
    var F = function(){}; //利用一个空对象去转接prototype,而且空对象几乎不占内存,不会影响父对象
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    
    Child.prototype.constructor = Child; //纠正回来它的构造函数指向
    Child.uber = Parent.prototype; //辅助属性,可以直接调用父的方法
  }

    function Animal(){ }
  Animal.prototype.species = "动物";

    function Cat(name,color){
        this.name=name
        this.color=color
    }

    extend(Cat,Animal);

  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

拷贝继承


    function extend2(Child, Parent) {
    
    var p = Parent.prototype;
    var c = Child.prototype;
    
    for (var i in p) { //循环父的原型方法给到子
      c[i] = p[i];
    }

    c.uber = p;

  } 

    extend2(Cat, Animal);

  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

原型和原型链

私有变量和函数
在函数内部定义的变量和函数,叫局部(内部)变量和函数,如果不对外提供接口,外部是无法访问到的。

function Box(){
    var color = "blue"; //私有变量
    var fn = function(){} //私有函数
}
var obj = new Box();
    alert(obj.color); //弹出 undefined,访问不到私有变量
    alert(obj.fn); //同上

静态变量和函数
定义一个函数后加"."来添加的属性和函数,该函数可以访问到,但实例访问不到。

function Obj(){};

Obj.num = 72; //静态变量
Obj.fn = function() { } //静态函数
  
alert(Obj.num); //72
alert(typeof Obj.fn) //function

var t = new Obj();
alert(t.name); //undefined
alert(typeof t.fn); //undefined

实例变量和函数

function Box(){
    this.a=[]; //实例变量
    this.fn=function(){} //实例方法
}

var box1=new Box();
box1.a.push(1);
box1.fn={};
console.log(box1.a); //[1]
console.log(typeof box1.fn); //object

var box2=new Box();
console.log(box2.a); //[]
console.log(typeof box2.fn); //function

box1中修改了afn,而在box2中没有改变,由于数组和函数都是对象,是引用类型,这就说明box1中的属性和方法与box2中的属性与方法虽然同名但却不是一个引用,而是对Box对象定义的属性和方法的一个复制。

我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。那么,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

使用原型的好处是可以让对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中添加定义对象信息,而是可以直接将这些信息添加到原型中。使用构造函数的主要问题就是每个方法都要在每个实例中创建一遍。

JavaScript中,一共有两种类型的值,原始值和对象值。每个对象都有一个内部属性 prototype ,我们通常称之为原型。原型的值可以是一个对象,也可以是null。如果它的值是一个对象,则这个对象也一定有自己的原型。这样就形成了一条线性的链,我们称之为原型链。

函数可以用来作为构造函数来使用。另外只有函数才有prototype属性并且可以访问到,但是对象实例不具有该属性,只有一个内部的不可访问的__proto__属性。__proto__是对象中一个指向相关原型的神秘链接。按照标准,__proto__是不对外公开的

当调用构造函数创建一个实例的时候,实例内部将包含一个内部指针(__proto__)指向构造函数的prototype,这个连接存在于实例和构造函数的prototype之间,而不是实例与构造函数之间。

function Person(name){                             //构造函数
    this.name=name;
}

Person.prototype.printName=function() {//原型对象
    alert(this.name);
}

var person1=new Person('Byron'); //实例化对象
console.log(person1.__proto__); //Person
console.log(person1.constructor); //Person
console.log(Person.prototype); //指向原型对象Person
var person2=new Person('Frank');

Person的实例person1中包含了name属性,同时自动生成一个__proto__属性,该属性指向Personprototype,可以访问到prototype内定义的printName方法
图片描述

实例就是通过构造函数创建的。实例一创造出来就具有constructor属性(指向构造函数)和__proto__属性(指向原型对象),

构造函数中有一个prototype属性,这个属性是一个指针,指向它的原型对象。
原型对象内部也有一个指针(constructor属性)指向构造函数:Person.prototype.constructor = Person;

实例可以访问原型对象上定义的属性和方法。
在这里person1person2就是实例,prototype是他们的原型对象。

原型链的示意图可以用下图来表示:
图片描述

ES6


-

基本上,ES6class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

class Point { //类名
    constructor(x, y) { //构造函数,constructor方法默认返回实例对象(即this)
        this.x = x; //this关键字代表实例对象
        this.y = y;
    }
    toString() { //prototype原型对象
        return '(' + this.x + ', ' + this.y + ')';
    }
}

typeof Point // "function"
Point === Point.prototype.constructor // true

    

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

let point = new Point(1,2); //也是直接使用new命令,传给constructor的值
point.toString(); //(1,2)
point.hasOwnProperty('x') //true,自身的属性(因为定义在this变量上),
point.hasOwnProperty('toString') //false/此属性是定义在原型上
point.__proto__.hasOwnProperty('toString') //true

point.constructor === Point.prototype.constructor //true

在类的实例上面调用方法,其实就是调用原型上的方法。

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

class Point {
    constructor(){ //可以忽略不写,会自动添加
        // ...
    }
}
Object.assign(Point.prototype, {
    toString(){},
    toValue(){}
});

prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的。

Point.prototype.constructor === Point //true,类内部的方法不能枚举,和es5不一样

继承

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

class ColorPoint extends Point {}//相当于var ColorPoint=new Point也就是ColorPoint继承了Point

上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。es6的继承是先新建父类的实例,再在子类中继承修改this的指向

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); //调用父类的constructor(x, y)给父类传值,用来新建父类的this对象。
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // ES6 规定,通过super调用父类的方法时,super会绑定子类的this。
  }
}

let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

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

继承链
大多数浏览器的ES5实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。ES6同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {}

class B extends A {}

B.__proto__ === A  //true,es5中,是constructor,实例
B.prototype.__proto__ === A.prototype  //true,es5中,是B._proto_===A.prototype,方法

这样的结果是因为,类的继承是按照下面的模式实现的。

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype) = B.prototype.__proto__ = A.prototype;
const b = new B();

// B的实例继承A的静态属性
Object.setPrototypeOf(B, A) = B.__proto__ = A;
const b = new B();

《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
    实例的__proto__属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

三种特殊情况。

第一种特殊情况,子类继承Object类。

class A extends Object {}

A.__proto__ === Object //true
A.prototype.__proto__ === Object.prototype //true

这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。

第二种特殊情况,不存在任何继承。

class A {}
//因为A就是一个函数,所以它继承的自然就是函数。就相当于new Function,但它的prototype是一个对象,所以继承自对象

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。

第三种特殊情况,子类继承null

class A extends null {}

A.__proto__ === Function.prototype // true,表明new出来的都是函数,A是函数
A.prototype.__proto__ === undefined // true,因为继承自null,所以它的_proto_找不着,就是undefined

这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回的对象不继承任何方法,所以它的__proto__指向Function.prototype,即实质上执行了下面的代码。

class C extends null {
  constructor() { return Object.create(null); 
}

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

以前,这些原生构造函数是无法继承的。

class MyDate extends Date{

    getTest(){
        console.log('我是MyDate的扩展方法',this===date,new Date(),new MyDate(),)
        // this向的是它的实例对象,this===date
    }
}

let date=new MyDate();
console.log(date.getTime());//本地时间
date.getTest() //我是MyDate的扩展方法 true


Waxiangyu
670 声望30 粉丝