阮一峰的JavaScript 教程读书笔记之面向对象编程

时间:2024-10-04 20:58:33

文章目录

  • OOP
    • 实例对象与new命令
      • 构造函数
      • new命令
    • this 关键字
      • 实质
      • 使用场合
        • (1)全局变量
        • (2)构造函数
        • (3)对象的方法
      • 使用注意
        • 1.避免函数多层嵌套中使用this
        • 2.避免数组处理中的`this`
      • this绑定
        • call函数
        • apply函数
        • bind函数
    • 对象的继承
      • 原型对象
        • 原型链
      • instanceof运算符
      • 构造函数的继承
        • 两种继承的区别
          • 原型继承
          • 寄生组合式继承
      • 模块
    • Object 对象的相关方法
      • ()
      • ()
      • ()
      • .__proto__
      • 5.获取原型方法的比较
      • ()
      • 和for...in

OOP

实例对象与new命令

构造函数

JS中的面向对象是通过构造函数(constructor)和原型链(prototype)来实现的。JavaScript 语言使用构造函数(constructor)作为对象的模板。

var Vehicle = function () {
  this.price = 1000;
};
  • 1
  • 2
  • 3
'
运行

为了与普通函数区别,构造函数名字的第一个字母通常大写。

  • 函数体内使用this关键词代表了所要生成的对象实例
  • 生成对象的时候,必须使用new命令

new命令

new 命令就是生成一个实例对象。

var v = new Vehicle();
v.price // 1000
  • 1
  • 2

new命令执行时,构造函数内部的this,就代表了新生成的实例对象,表示实例对象有一个price属性,值是1000。

可以通过在构造函数第一行加上use strict以防止不使用new,直接调用函数,此时会报错。同时也可以使用属性来判断是否通过new命令调用构造函数。

function f() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用!');
  }
  // ...
}

f() // Uncaught Error: 请使用 new 命令调用!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

new 命令的原理
(1)创建一个空对象,作为要返回的对象实例
(2)将这个空对象的原型,指向构造函数的prototype属性
(3)将这个空对象赋值给函数内部的this关键词
(4)继续执行构造函数内部操作

构造函数之所以叫构造函数,是说这个函数的目的是操作空对象,构造成需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

//返回this对象的例子:
var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000// false

//返回对象的例子:
var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
'
运行

有时候我们拿不到构造函数,可以使用方法来创建实例对象,person2继承了person1的方法和属性。

var person1 = {
  name: '张三',
  age: 38,
  greeting: function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};

var person2 = Object.create(person1);

person2.name // 张三
person2.greeting() // Hi! I'm 张三.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
'
运行

this 关键字

简单说,this就是属性或方法“当前”所在的对象。

由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,this的指向是可变的

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

B.describe = A.describe;
B.describe()
// "姓名:李四"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
'
运行

上面代码中,属性被赋给B,于是就表示describe方法所在的当前对象是B,所以就指向

只要函数赋值给另一个变量,this指向就会变。下述例子中,被赋值给f后,就会选择f运行时所在的name

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var name = '李四';
var f = A.describe;
f() // "姓名:李四"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。

实质

看似简单的一句代码,其实JS引擎会在内存中存储一个对象{foo:5},然后让obj指向该内存地址(或者说obj就是一个地址),所以当访问时,会首先从内存地址找到那个对象,然后再去拿他的foo属性。

var obj = { foo:  5 };
  • 1
'
运行

在内存中该对象的存储形式为字典形式。

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

当对象中属性不是一个值而是一个函数时,会将函数的地址存储到value中。

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

而函数可能在不同的环境下执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
'
运行

JavaScript 允许在函数体内部,引用当前环境的其他变量。比如下列函数中的x变量,允许由环境来提供。

var f = function () {
  console.log(x);
};
  • 1
  • 2
  • 3
'
运行

设计this的原因,是要能够在函数体内部获得当前的运行环境(找到调用函数所在的当前环境)。

因此在f函数中会使用this来指代当前的运行环境。下面为一个典型的例子,如果单独执行的话,属于在最外层运行环境,因此x是取全局变量1;而如果是在obj环境执行,则x是取局部变量2。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
'
运行

使用场合

(1)全局变量

全局环境使用this,它指的就是顶层对象window。

this === window // true

function f() {
  console.log(this === window);
}
f() // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
(2)构造函数

构造函数中使用this,它指的是实例对象。

var Obj = function (p) {
  this.p = p;
};
var o = new Obj('Hello World!');
o.p // "Hello World!"
  • 1
  • 2
  • 3
  • 4
  • 5
'
运行
(3)对象的方法

如果对象的方法里面包含thisthis的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。

这个很难理解,先看下面的例子,此时调用结果是obj,因为方法运行时所在的对象或环境就是obj。

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
'
运行

而接下来三种情况都是最外层的window,可以这样理解:()相当于是从obj的地址去调用foo的地址,所以运行环境是在obj,因此this指向obj。
而下述三种情况都是直接去调用的foo,可以理解为直接从window调用的,所以this就指向window了。

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上述三种情况等同于:

// 情况一
(obj.foo = function () {
  console.log(this);
})()
// 等同于
(function () {
  console.log(this);
})()

// 情况二
(false || function () {
  console.log(this);
})()

// 情况三
(1, function () {
  console.log(this);
})()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

这里的this指代的就是b了,而不是a。

var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined
//等价于
var b={
	m:function() {
      console.log(this.p);
    }
}
var a ={
	p:'hello'
	b:b

a.b.m()//undefined
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

使用注意

1.避免函数多层嵌套中使用this
var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()
// Object
// Window
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
'
运行

上述代码可中第一个是Object没有问题,因为是对象o调用的他,而中间的f2函数相当于没有人调用它而自己执行的,所以它的this就指向了最外层的window。

所以上述代码类似于:

var temp = function () {
  console.log(this);
};

var o = {
  f1: function () {
    console.log(this);
    var f2 = temp();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

对应的一个解决方法是在第二层改用一个指向外层this的变量。

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
'
运行

上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。

2.避免数组处理中的this

在f方法中的第二层this其实和第一种情况相似,仍旧不是指的外部,而是指向了顶部(window)。因此同样可以考虑使用一个变量来固定指向外层的this。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// undefined a1
// undefined a2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
'
运行

解决方法:在外层使用变量,来固定内层的this(内层不用this而用that)。

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
'
运行

this绑定

JavaScript 提供了callapplybind这三个方法,来切换/固定this的指向。

call函数

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数

上述代码直接指定f函数的执行作用域为obj

var obj = {};

var f = function () {
  return this;
};

f() === window // true
f.call(obj) === obj // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

同时call函数可以传入多个参数,第一个参数代表作用域,而剩下的个数代表在函数调用时所需的参数。。

func.call(thisValue, arg1, arg2, ...)
//一个例子:
function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

call方法的一个应用是调用对象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

上面代码中,hasOwnPropertyobj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。

apply函数

apply和call函数的区别主要在于,apply函数要求传入一个对象,一个数组。也就是说通过数组来存放函数要求的入参。

func.apply(thisValue, [arg1, arg2, ...])

  • 1
  • 2

(1)可以通过apply函数和函数来找到数组中最大的元素:

var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
  • 1
  • 2
'
运行

(2)将数组的空元素变成undefined
可以通过apply方法,利用Array构造函数将数组的空元素变成undefined

Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
  • 1
  • 2
'
运行

可以理解undefinednull最大的区别是可以被for each遍历到。

var a = ['a', , 'b'];

function print(i) {
  console.log(i);
}

a.forEach(print)
// a
// b

Array.apply(null, a).forEach(print)
// a
// undefined
// b
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
'
运行
bind函数

bind()方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

下述代码出现错误的问题是getTime中存在this,而this绑定的是一个Date对象,而赋给print对象后,内部的this就指向了print对象

var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
  • 1
  • 2
  • 3
  • 4
  • 5

我们可以使用bind函数将getTime内部的this绑定到对象d上,然后再返回这个函数,就可以随便赋值给其他对象。

var print = d.getTime.bind(d);
print() // 1481869925657
  • 1
  • 2

接下来是一个很直观的例子。我们让inc方法中的this绑定在counter上,然后返回,否则当前执行就会出错。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

还是上面的例子,我们也可以把inc函数中的this绑定到其他对象上,这样在调用时就用当前对象环境内的count值了,也就是100.

var obj = {
  count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

bind()还可以接受更多的参数,将这些参数绑定原函数的参数。

这个5是相当于是给x预先赋值为5了,所以在传入一个y就可以了,我刚开始看了半天,给爷看蒙了都。

var add = function (x, y) {
  return x * this.m + y * this.n;
}

var obj = {
  m: 2,
  n: 2
};

var newAdd = add.bind(obj, 5);
newAdd(5) // 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
'
运行

因此同理,我们可以相当于再造一个函数,就是通过预先赋值来造一个固定+5的函数

function add(x, y) {
  return x + y;
}

var plus5 = add.bind(null, 5);
plus5(10) // 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
'
运行

注意事项:
(1)每一次返回一个新函数
我们很快就能想到匿名函数不能用这个,否则每次都生成个新的,而且还没法取消绑定

//错误的
element.removeEventListener('click', o.m.bind(o));
//正确的
var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(2)结合回调函数使用
学android的人可太能理解回调函数了,在JS中最常犯的错误是直接将包含this的方法直接返回当做回调函数。

解决方法就是使用bind()方法,将()绑定counter。可以理解为如果不将inc绑定到counter对象上,他就会调用当前环境(window)的count了

var counter = {
  count: 0,
  inc: function () {
    'use strict';
    this.count++;
  }
};

function callIt(callback) {
  callback();
}

callIt(counter.inc.bind(counter));
counter.count // 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
'
运行

(3)结合call方法使用

对象的继承

原型对象

每新建一个对象时,就会新建构造函数中的所有方法,这样会浪费系统资源,例如下方代码中的meow

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log('喵喵');
  };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
'
运行

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。

function f() {}
typeof f.prototype // "object"
  • 1
  • 2
'
运行

对于普通函数来说,该属性基本无用。对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

下述代码中,就是实例对象cat1cat2对象的原型对象。

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"
  • 1
  • 2
  • 3
  • 4

当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
  • 1
  • 2
  • 3
  • 4
  • 5
原型链

JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”。

所有对象的最顶层都是,这也是为什么所有对象都有toStringvalueOf方法。

JS引擎在寻找一个属性或方法时会首先寻找当前对象是否包含,如果没有的话会顺着原型链逐步寻找,直到找到中也没有,就会返回undefined。如果对象自身和原型中都有一个同名属性或方法,叫做覆盖(override)。

如果我们让对象的prototype指向一个数组实例,那么该对象就可以使用数组的方法。

var MyArray = function () {};

MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
'
运行

prototype对象有一个constructor属性,默认指向prototype对象对象所在的构造函数。

function P() {}
P.prototype.constructor === P // true
  • 1
  • 2
'
运行

上面代码中,p是构造函数P的实例对象,但是p自身没有constructor属性,该属性其实是读取原型链上面的属性。

function P() {}
var p = new P();
p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
  • 1
  • 2
  • 3
  • 4
  • 5
'
运行

constructor可以让我们知道一个对象的构造函数,同时有了constructor,我们就可以通过一个对象生成另一个对象。

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true
  • 1
  • 2
  • 3
  • 4
  • 5
'
运行

constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错。

function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
'
运行

上述代码出错的原因是由于我们修改了Person构造函数的prototype对象,而没修改constructor属性,导致constructor属性就指向了Object构造函数(由于Person的新原型是一个普通对象,而普通对象的constructor属性指向Object构造函数)。

所以我们在修改原型对象时,一定要记得修改它的constructor属性。当然最好还是只修改原型对象的函数,而不修改原型对象。

// 坏的写法
C.prototype = {
  method1: function (...) { ... },
  // ...
};

// 好的写法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
};

// 更好的写法
C.prototype.method1 = function (...) { ... };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

instanceof运算符

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

var v = new Vehicle();
v instanceof Vehicle // true
  • 1
  • 2

instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上

注意运算符检查的是整个原型链,所以可能一个对象是好多个构造函数的实例。

var d = new Date();
d instanceof Date // true
d instanceof Object // true
  • 1
  • 2
  • 3
'
运行

由于任意对象(除了null)都是Object的实例,所以instanceof运算符可以判断一个值是否为非null的对象。

var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false
  • 1
  • 2
  • 3
  • 4
'
运行

instanceof还可用于判断值的类型。

var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true
  • 1
  • 2
  • 3
  • 4
'
运行

但切记instanceof只能判断对象的类型,而不可以判断基本值的类型。

下面例子返回false的原因是因为s不是String的实例对象。

var s = 'hello';
s instanceof String // false
  • 1
  • 2
'
运行

构造函数的继承

先说一下构造函数的继承要干啥:首先简单来说就是子类构造函数要有父类的东西,也要有父类的父类(原型链)的东西,因此构造函数的继承可以分成两步:
(1)在子类的构造函数中,调用父类的构造函数
下述代码中,Sub是子类的构造函数,而Super是父类的构造函数,我们通过call函数可以指定Super函数内部的this指向,然后在当前的作用域中,调用该函数。这样子类就会有父类的属性

function Sub(value) {
  Super.call(this);
  this.prop = value;
}
  • 1
  • 2
  • 3
  • 4
'
运行

(2)让子类的原型指向父类的原型,这样子类就可以继承父类原型
注意:这里不需要让子类的原型指向父类了,第一步已经不是拿了么?还指向锤子哟。
要记得更改原型对象,一定要把原型对象的constructor属性也更改回自己,否则会出问题。而且记得更改原型对象别直接去,那相当于直接指向了父类原型对象的内存地址,你一改prototypeconstructor属性,父类的也改了。

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
  • 1
  • 2
  • 3

另外一种写法是直接指向一个父类对象,这样子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法

Sub.prototype = new Super();
  • 1

继承的例子:
父类是Shape

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行

我们需要让Rectangle构造函数继承Shape

// 第一步,子类继承父类的实例
function Rectangle() {
  Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
  this.base = Shape;
  this.base();
}

// 第二步,子类继承父类的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

采用这样的写法以后,instanceof运算符会对子类和父类的构造函数,都返回true。

var rect = new Rectangle();

rect instanceof Rectangle  // true
rect instanceof Shape  // true
  • 1
  • 2
  • 3
  • 4

之前我有怀疑说这种方式是让子类的原型对象指向了父类的原型对象,为什么instanceof Shape会返回true呢?
因为instanceOf会走一遍rect的原型链,看Shape的原型对象是否在左边对象的原型链上。
rect的原型链:rect->()->。
所以切记一点是Shape的原型对象而不是Shape是否在左边对象的原型链上!!!

两种继承的区别

最后加个餐,我还是没太理解这两种继承的区别,所以我搜索了篇文章:【设计模式+原型理解】第三章:javascript五种继承父类方式
其实两种方式分别叫做原型继承和寄生组合式继承。

原型继承

简单理解原型继承的核心代码只需要让Bprototype指向一个新的父类对象即可。

B.prototype = new A;
B.prototype.constructor = B;
  • 1
  • 2

完整代码如下,这种方式会让B继承A的所有私有和公有的属性和方法。

// 原型继承简化<br>function A() {
    this.x = 100;
}
A.prototype.getX = function () {
    console.log(this.x)
};
function B() {
    this.y = 200;
}
// 现在,B想继承A的私有+公有的属性和方法
B.prototype = new A;
B.prototype.constructor = B;
var b = new B;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

原型继承并不是把父类中的属性和方法克隆一份一模一样的给B,而是让B和A之间增加原型链的连接,以后实例b想要A中的getX方法,需要一级级向上查找来使用。

b 对象 的 proto 属性对应的的就是 A构造函数 的 prototype 属性

在这里插入图片描述

寄生组合式继承

思路是子类继承父类,父类私有的子类就继承私有的,父类公有的子类就继承公有的。

核心代码如下:
继承私有:通过call来让父类私有的东西(属性和方法)在子类私有里(B的内部),
继承公有:新建一个新对象,然后新对象的原型指向父类的原型,再让子类的原型指向该对象。(好处就是只有公有的属性和方法了)

function B() {
    // ->this->b
    A.call(this);
}
//  = (); // 意思是把父类的原型,给了子类的原型
// 创建了一个对象,并且把这个新对象的原型指向了a的原型,然后B的原型指向了这个新对象
B.prototype = objectCreate(A.prototype);
B.prototype.constructor = B;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述

模块

但是,JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果。那就直接看ES6被。

Object 对象的相关方法

()

方法返回参数对象的原型。这是获取原型对象的标准方法。

var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
  • 1
  • 2
  • 3
'
运行

()

方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
new命令可以使用方法模拟。

var F = function () {
  this.foo = 'bar';
};

var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
'
运行

第一步,将一个空对象的原型设为构造函数的prototype属性(上例是);第二步,将构造函数内部的this绑定这个空对象,然后执行构造函数,使得定义在this上面的方法和属性(上例是),都转移到这个空对象上。

()

该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
'
运行

方法可以用以下代码代替。由此可知该方法的实质是首先创建一个新的构造函数F,让构造函数的原型对象prototype为obj,这样再创建一个实例对象就可以继承obj的所有属性和方法。

if (typeof Object.create !== 'function') {
  Object.create = function (obj) {
    function F() {}
    F.prototype = obj;
    return new F();
  };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
'
运行

()方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。

var obj = Object.create({}, {
  p1: {
    value: 123,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  p2: {
    value: 'abc',
    enumerable: true,
    configurable: true,
    writable: true,
  }
});

// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
'
运行

.proto

实例对象的__proto__属性(前后各两个下划线),返回该对象的原型。该属性可读写。

var obj = {};
var p = {};

obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true
  • 1
  • 2
  • 3
  • 4
  • 5
'
运行

可以通过这个属性来设置对象的原型对象,但它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用()(),进行原型对象的读写操作。

5.获取原型方法的比较

获取实例对象obj的原型对象,有三种方法。

  • obj.__proto__
  • (obj)
    一般推荐使用第三种方式获取原型对象。因为第一种可能不会部署,第二种可能会随着在更改原型对象时失效。
var P = function () {};
var p = new P();

var C = function () {};
C.prototype = p;
var c = new C();

c.constructor.prototype === p // false
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
'
运行

这是因为在更改C的原型对象时,没有更改它的constructor属性。

C.prototype = p;
C.prototype.constructor = C;

var c = new C();
c.constructor.prototype === p // true
  • 1
  • 2
  • 3
  • 4
  • 5

()

对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。

Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // false
  • 1
  • 2
'
运行

和for…in

in运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性。

'length' in Date // true
'toString' in Date // true
  • 1
  • 2
'
运行

in运算符常用于检查一个属性是否存在。获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用for...in循环。如果想要只遍历自己的属性可以加上hasOwnProperty判断。

for ( var name in object ) {
  if ( object.hasOwnProperty(name) ) {
    /* loop code */
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5