JS面向对象

227 阅读16分钟

一、认识对象

1、认识对象

  • 定义:对象(object)是“键值对”的集合,表示属性和值的映射关系
var xiaoming = {
    name: '小明',
    age: 12,
    sex: '男',
    hobbies: ['足球', '编程']
};

{key:value},大括号表示对象,属性名(键名,key):属性值(value)

  • 语法:kv之间用冒号分割,每组k:v之间用逗号分割,最后一个k:v对后可以不书写逗号
var obj = {
    k: v,
    k: v,
    k: v
};
  • 如果对象的属性键名不符合JS标识符命名规范,则这个键名必须用引号包裹
var xiaoming = {
    name: '小明',
    age: 12,
    sex: '男',
    hobbies: ['足球', '编程'],
    'favorite-book': '舒克和贝塔'
};

例如:favorite-book不符合JS标识符的命名规范,要用引号包裹

  • 属性的访问:可以用“点语法”访问对象中指定键的值
xiaoming.name;  // '小明'
xiaoming.age;  // 12
xiaoming.hobbies;  // ['足球', '编程']
  • 如果属性名不符合JS标识符命名规范,则必须使用方括号的方法来访问
xiaoming['favorite-book'];  // '舒克和贝塔'
  • 如果属性名以变量形式存储,则必须使用方括号形式
var obj = {
    a: 1,
    b: 2,
    c: 3
};

var key = 'b';
console.log(obj.key);  // undefined
console.log(obj[key]);  // 2
  • 属性的更改:直接使用赋值运算符重新对某属性赋值即可更改属性
var obj = {
    a:10
};
obj.a = 30;  // 30
obj.a++;  // 31
  • 属性的创建:如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来
var obj = {
    a: 10
};
obj.b = 40;
  • 属性的删除:如果要删除某个对象的属性,需要使用delete操作符
var obj = {
    a: 1,
    b: 2
};
delete obj.a;

2、对象的方法

  • 对象的方法:如果某个属性值是函数,则它也被称为对象的“方法”
var xiaoming = {
    name: '小明',
    age: 12,
    sex: '男',
    hobbies: ['足球', '编程'],
    'favorite-book': '舒克和贝塔',
    sayHello: function () {
        console.log('你好,我是小明,今年12岁,我是个男生');
    }
};

sayHello就是xiaoming对象的方法

  • 方法的调用:使用“点语法”可以调用对象的方法
xiaoming.sayHello();  // '你好,我是小明,今年12岁,我是个男生'
  • 方法和函数的关系:方法也是函数,只不过方法是对象的“函数属性”,它需要用对象打点调用

3、对象的遍历

  • for...in...循环
var obj = {
    a: 1,
    b: 2,
    c: 3
};

for (var k in obj) {
    console.log('属性' + k + '的值是' + obj[k]);
};

4、对象的深浅克隆

  • 基本类型值和引用类型值
举例var a = b变量传值时当用==比较时
基本类型值数字、字符串、布尔、undefinednull内存中产生新的副本比较值是否相等
引用类型值对象、数组等内存中不产生新的副本,而是让新变量指向同一个对象比较内存地址是否相同,即比较是否为同一对象
  • 对象是引用类型值,这意味着:

① 不能用var obj2 = obj1这样的语法克隆一个对象,因为不会创建一个新的对象
② 使用==或者===进行对象的比较时,比较的是它们是否为内存中的同一对象,而不是比较值是否相同

var obj1 = {
    a: 1,
    b: 2,
    c: 3
};

var obj2 = {
    a: 1,
    b: 2,
    c: 3
};

console.log(obj1 == obj2);  // false
console.log(obj1 === obj2);  // false
/* 因为对象是引用类型值,既要比较值也要比较内存地址,
obj1和obj2是在两个不同的堆内存中 */
  • 什么是浅克隆:只克隆对象的“表层”,如果对象的某些属性又是引用类型值,则不进一步克隆它们,只是传递它们的引用

  • 使用for...in...循环即可实现对象的浅克隆

var obj1 = {
    a: 1,
    b: 2,
    c: [44, 55, 66]
};

var obj2 = {};

for(var k in obj1) {
    obj2[k] = obj1[k];
};

/* 为什么叫浅克隆呢?比如c属性的值是引用类型值,
那么本质上obj1和obj2的c属性是内存中的同一数组,
并没有被克隆分开。修改c属性的值仍然会同时变化*/
  • 什么是深克隆:克隆对象的全貌,不论对象的属性值是否是引用类型值,都能将它们实现克隆

  • 和数组的深克隆类似,对象的深克隆需要使用递归

var obj1 = {
    a: 1,
    b: 2,
    c: [
        33,
        44,
        {
            m: 55,
            n: 66,
            p: [77, 88]
        }
    ]
};

function deepClone(o) {
    // 判断o是对象还是数组,注意数组的typeof的结果也是'object'
    if(Array.isArray(o)) {
        // 数组
        var result = [];
        for (var i = 0; i < o.length; i++) {
            result.push(deepClone(o[i]));
        }
    } else if(typeof o == 'object') {
        // 对象
        var result = {};
        for (var k in o) {
            result[k] = deepClone(o[k]);
        }
    } else {
        // 基本数据类型
        var result = o;
    }
    
    return result;
}

var obj2 = deepClone(obj1);

二、认识函数的上下文

前置知识

  • 函数中可以使用this关键字,它表示函数的上下文
var xiaoming = {
    nickname: '小明',
    age: 12,
    sayHello: function () {
        console.log('我是' + this.nickname + ', 我' + this.age + '岁了')
    }
};

// xiaoming.sayHello();
var sayHello = xiaoming.sayHello;
sayHello();  // '我是undefined,我undefined岁了',这种情况xiaoming对象中的this不会指代xiaoming对象,而是指代window对象
  • 函数的上下文由调用方式决定,同一函数,用不同的形式调用它,则函数的上下文不同

①情形1:对象打点调用函数,函数中的this指代这个打点的对象

xiaoming.sayHello();

②情形2:圆括号直接调用函数,函数中的this指代window对象

var sayHello = xiaoming.sayHello;
sayHello();

1、上下文规则1

  • 对象打点调用它的方法函数,则函数的上下文是这个打点的对象
    对象.方法()
function fn() {
    console.log(this.a + this.b);
}
var obj = {
    a: 66,
    b: 33,
    fn: fn
}

obj.fn();  // 99
var obj1 = {
    a: 1,
    b: 2,
    fn: function () {
        console.log(this.a + this.b);
    }
};

var obj2 = {
    a: 3,
    b: 4,
    fn: obj1.fn
};

obj2.fn();  // 7
function outer() {
    var a = 11;
    var b = 22;
    return {
        a: 33,
        b: 44,
        fn: function () {
            console.log(this.a + this.b);
        }
    };
}

outer().fn();  // 77 

2、上下文规则2

  • 圆括号直接调用函数,则函数的上下文是window对象
    函数()
var obj1 = {
    a: 1,
    b: 2,
    fn: function () {
        console.log(this.a + this.b);
    }
};

var a = 3;
var b = 4;

var fn = obj1.fn;
fn();  // 7

3、上下文规则3

  • 数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)
    数组[下标]()
var arr = ['A', 'B', 'C', function () {
    console.log(this[0]);
}];

arr[3]();  // 'A'
  • 类数组对象:所有的键名为自然数序列(从0开始),且有length属性的对象

  • arguments对象是最常见的类数组对象,它是函数的实参列表

function fun() {
    arguments[3]();
}

fun('A', 'B', 'C', function () {
    console.log(this[1]);
});
//  'B'

4、上下文规则4

  • IIFE中的函数,上下文是window对象
    (function() {})();
var a = 1;
var obj = {
    a: 2,
    fun: (function () {
        var a = this.a;
        return function () {
            console.log(a + this.a);
        }
    })() // 这儿适用规则4
};
obj.fun();  // 这儿适用规则1,所以结果是3

5、上下文规则5

  • 定时器、延时器调用函数,上下文是window对象
    setInterval(函数, 时间);
    setTimeout(函数, 时间);
var obj = {
    a: 1,
    b: 2,
    fun: function () {
        console.log(this.a + this.b);
    }
};
var a = 3;
var b = 4;

setTimeout(obj.fun, 2000);  // 7

setTimeout(function() {
    obj.fun();  // 这里适用规则1,结果是3
}, 2000);

6、上下文规则6

  • 事件的处理函数的上下文是绑定时间的DOM元素
    DOM元素.onclick = function () {};

7、callapply

  • callapply能指定函数的上下文
    函数.call(上下文)
    函数.apply(上下文)
function sum() {
    alert(this.chinese + this.math + this.english);
}

var xiaoming = {
    chinese: 80,
    math: 95,
    english: 93
};

sum.call(xiaoming);
sum.apply(xiaoming);
  • callapply的区别
function sum(b1, b2) {
    alert(this.chinese + this.math + this.english + b1 +b2);
}

var xiaoming = {
    chinese: 80,
    math: 95,
    english: 93
};

sum.call(xiaoming, 5, 3);  // call 要用逗号罗列参数
sum.apply(xiaoming, [5, 3]);  // apply 要把参数写到数组中

sum.call(xiaoming);
sum.apply(xiaoming);
  • 到底使用call还是apply
function fun1() {
     fun2.apply(this, arguments);  // arguments 是类数组对象,所以要用apply
}

function fun2(a, b) {
    alert(a + b);
}

fun1(33, 44);

8、上下文规则总结

规则上下文
对象.函数()对象
函数()window
数组[下标]()数组
IIFEwindow
定时器window
DOM事件处理函数绑定DOM的元素
callapply任意指定
new调用函数秘密创建出的对象

三、构造函数

1、用new调用函数的四步走

  • 函数调用方式
    new 函数()

  • JS规定,使用new操作符调用函数会进行“四步走”
    ① 函数体内会自动创建出一个空白对象
    ② 函数的上下文this会指向这个对象
    ③ 函数体内的语句会执行
    ④ 函数会自动返回上下文对象,即使函数没有return语句

function fun() {
    this.a = 3;
    this.b = 4;
}

var obj = new fun();
console.log(obj);  // 1.{}; 2.this->{}; 3.{a: 3, b: 5}; 4.obj = {a: 3, b: 5}

2、构造函数 (缺失)

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayHello = function () {
        console.log('你好,我是' + this.name + ',我今年' + this.age + '岁了,我是个' + this.sex + '生');
    };
    this.sleep = function () {
        console.log(this.name + '开始睡觉,zzzzz');
    };
}

var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 4, '女');
var xiaogang = new People('小刚', 17, '男');

// console.log(xiaoming);
// console.log(xiaohong);
// console.log(xiaogang);

xiaohong.sayHello();
xiaogang.sleep();

3、类和实例

  • JavaC++等是“面向对象”(object-oriented)语言
  • JavaScript是“基于对象”(object-based)语言
  • JavaScript中的构造函数可以类比于面向对象语言中的“类”,写法的确类似,但和真正的面向对象语言还是有本质不同,JS有和其他面向对象语言完全不同的、特有的原型特性

四、原型和原型链

1、prototype和原型链查找

  • 什么是prototype:任何函数都有prototype属性,prototype的英语“原型”的意思

  • prototype属性值是个对象,它默认拥有constructor属性并指回函数

  • 对普通函数来说prototype属性没有任何用处,而构造函数的prototype属性非常有用

  • 构造函数的prototype属性是它的实例的原型
    image.png

  • JavaScript规定:实例可以打点访问它的原型的属性和方法,这被称为“原型链查找”

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
People.prototyle.nationality = '中国';

var xiaoming = new People('小明', 12, '男');
console.log(xiaming.nationality);  // '中国'
  • hasOwnProperty方法可以检查对象是否真正“自己拥有”某些属性或者方法
xiaoming.hasOwnProperty('name');  // true
xiaoming.hasOwnProperty('age');  // true
xiaoming.hasOwnProperty('sex');  // true
xiaoming.hasOwnproperty('nationality);  // false
  • in运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法
'name' in xiaoming;  true
'age' in xiaoming;  true
'sex' in xiaoming;  true
'nationality' in xiaoming;  true

2、在prototype上添加方法

  • 把方法直接添加到实例身上的缺点(添加在构造函数中):每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费

  • 解决方法是将方法写到prototype

function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

People.prototype.sayHello = function () {
    console.log('我是' + this.name);
};
People.prototype.sleep = function () {
    console.log(this.name + '开始睡觉,zzz');
};

3、原型链的终点Object.prototype

  • 对象的原型链

  • 数组的原型链

4、继承

  • 继承描述了两个类之间的"is a kind of"关系,比如学生“是一种”人,所以人类和学生类之间就构成继承关系

  • People是“父类”(或“超类”、“基类”);Student是“子类”(或“派生类”)

  • 子类丰富了父类,让类描述的更具体、更细化

  • JavasSript实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法

  • 使用JavaScript特有的原型链特性来实现继承,是普遍的做法

  • ES6中,有新的实现继承的方法

  • 通过原型链实现继承

// 父类,人类
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

People.prototype.sayHello = function () {
    console.log('我是' + this.name);
};
People.prototype.sleep = function () {
    console.log(this.name + '开始睡觉,zzz');
};

// 子类,学生类
function Student(name, age, sex, school, studentNumber) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.school = school;
    this.studentNumber = studentNumber;
}
//关键语句,实现继承,一定要写在,子类把方法添加到原型之前
Student.prototype = new People();
Student.prototype.study = function () {
    console.log(this.name + '正在学习');
}
Student.prototype.exam = function () {
    console.log(this.name + '正在考试');
}

// 重写、复写(override)父类的sayHello
Student.prototype.sayHello = function () {
    console.log('敬礼!我是' + this.name + '我今年' + this.age + '岁了');
}

// 实例化
var hanmeimei = new Student('韩梅梅', 9, '女', '中心小学', 100556);

hanmeimei.study();
hanmeimei.sayHello();
hanmeimei.sleep();

五、上升到面向对象

  • 面向对象的本质:定义不同的类,让类的实例工作

  • 面向对象的优点:程序编写更清晰,代码结构更严密、使代码更健壮更利于维护

  • 面向对象经常用到的场合:需要封装和复用性的场合(组件思维)

1、上升到面向对象案例1-红绿灯

  • 页面上做一个红绿灯,点击红灯就变黄,点击黄灯就变绿,点击绿灯就变回红灯 image.png

  • 使用面向对象编程,就能以“组件化”的思维解决大量红绿灯互相冲突的问题

  • 面向对象编程,最重要的就是编写类

  • TrafficLight
    请思考,红绿灯TrafficLight类有哪些属性和方法呢?
    属性:自己当前的颜色color,自己的DOM元素dom
    方法:初始化init()、切换颜色changeColor()、绑定事件bindEvent()

2、上升到面向对象案例2-炫彩小球

  • Ball类属性
属性名意义
x圆心坐标x
y圆心坐标y
r圆半径
opacity透明度
color颜色
domDOM元素
  • BALL类的方法
方法名意义
init初始化
update更新
  • 如何实现多个小球动画
    ① 把每个小球实例都放到同一数组中
    [{小球实例},{小球实例},{小球实例},{小球实例}]
    ② 只需要使用1个定时器,在每一帧遍历每个小球,调用它们的update方法

六、JS的内置对象

1、包装类

  • 什么是包装类:Number()String()Boolean()分别是数字、字符串、布尔值的“包装类”

  • 很多编程语言都有“包装类”的设计,包装类的目的就是为了让基本类型值可以从它们的构造函数的prototype上获得方法

var a = new Number(123);
var b = new String('北京市');
var c = new Boolean(true);

console.log(a);  // Number {123}
console.log(typeof a);  // object
console.log(b);  // String {"北京市"}
console.log(typeof b);  // object
console.log(c);  // Boolean {true}
console.log(typeof c);  // object

consle.log(5 + a);  // 128
console.log(b.slice(0,2));  // "北京"
console.log(c && true); // true

var d = 123;
console.log(d.__proto__ == Number.prototype);  // true
  • Number()String()Boolean()的实例都是object类型,它们的PrimitiveValue属性存储它们的本身值

  • new出来的基本类型值可以正常参与运算

  • 包装类的目的就是为了让基本类型值可以从它们的构造函数的prototype上获得方法

2、Math对象

  • 幂和开方:Math.pow()Math.sqrt()

  • 向上取整和向下取整:Math.ceil()Math.floor()

  • Math.round()可以将一个数字四舍五入为整数

console.log(Math.round(3.4));  // 3
console.log(Math.round(3.5));  // 4
console.log(Math.round(3.98));  // 4
console.log(Math.round(3.49));  // 3

// 四舍五入到指定位数
var a = 3.7554;
console.log(Math.round(a * 100) / 100);  // 3.76
  • Math.max()Math.min()可以得到参数列表的最大值和最小值
console.log(Math.max(6, 2, 9, 4));  // 9
console.log(Math.min(6, 2, 9, 4));  // 2
  • 结合apply方法可以利用Math.max()Math.min()求数组中的最大值最小值
var arr = [3, 6, 9, 2];
var max = Math.max.apply(null, arr);
var min = Math.min.apply(null, arr);
console.log(max);  // 9
console.log(min);  // 2
  • Math.random()可以得要0~1之间的小数
    为了得到[a, b]区间内的整数,可以使用这个公式:parseInt(Math.random() * (b - a +1)) + a

3、Date对象

  • 使用new Date()即可得到当前时间的日期对象,它是object类型值
    ① 使用new Date(2020, 11, 1)即可得到指定日期的日期对象,注意第二个参数表示月份,从0开始计算,11表示12月,这种方法不算时区,00:00:00 ② 也可以是new Date('2020-12-01')这样的写法。这种方法算时区,08:00:00

  • 日期的常见方法

方法功能
getDate()得到日期1~31
getDay()得到星期0~6
getMonth()得到月份0~11
getFullYear()得到年份
getHours()得到小时数0~23
getMinutes()得到分钟数0~59
getSeconds()得到秒数0~59
  • 时间戳表示1970年1月1日零点整距离某时刻的毫秒数
    ① 通过getTime()方法或者Date.parse()函数可以将日期对象变成时间戳
    ② 通过new Date(时间戳)的写法,可以将时间戳变为日期对象
// 日期对象
var d = new Date();

// 显示时间戳
var timestamp1 = d.getTime();
var timestamp2 = Date.parse(d);

console.log(timestamp1);  // 精确到毫秒
console.log(timestamp2);  // 精确到秒,也是毫秒数,只不过最后三位一定是000

// 如何吧时间戳,比如1601536565000变回日期对象呢?
var dd = new Date(1601536565000);
console.log(dd);

内置构造函数

1. 示意

  • JavaScript有很多内置构造函数,比如Array就是数组类型的构造函数,Function就是函数类型的构造函数。Object就是对象类型的构造函数
    内置构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的prototype上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能
// 数组的内置构造函数,任何的数组字面量都可以看做是Array的实例
console.log([1, 2] instanceof Array);  // true
console.log([] instanceof Array);  // true

var arr = new Array(5);
console.log(arr); // (5)[empty x 5]
console.log(arr.length); // 5

// 函数的内置构造函数,任何的函数字面量都可以看做是Function的实例
function fun() {}
function add(a, b) {
    return a + b;
}
console.log(fun instanceof Function);  // true
console.log(add instanceof Function);  // true

var jianfa = new Function('a', 'b', 'return a - b');
console.log(jianfa(8, 3));  // 5

// 对象的内置构造函数
console.log({a: 1} instanceof Object);  // true
console.log({} instanceof Object);  // true

var o = new Object();
o.a = 2;
o.b = 4;
console.log(o);
  • 如何扩展内置构造函数
console.log(Array.prototype.hasOwnProterty('push'));  // true
console.log(Array.prototype.hasOwnProterty('pop'));  // true
console.log(Array.prototype.hasOwnProterty('splice'));  // true

// 拓展qiuhe方法
Array.prototype.qiuhe = function () {
    // this 指代的调用qiuhe()方法的数组
    var arr = this;
    // 累加器
    var sum = 0;
    for (var i = 0;i < arr.length; i++ ) {
        sum += arrr[i];
    }
    // 返回结果
    return sum;
}

var arr = [3, 6, 2, 1, 3];
var result = arr.qiuhe();
console.log(result);  // 15

2、内置构造函数的关系

  • Object.prototype是万物原型链的终点
    Object.prototype是万物原型链的终点。JavaScript中函数、数组皆为对象,以数组为例,完整的原型链是这样的: image.png
console.log([1, 2].__proto__ === Array.prototype);  // true
console.log([1, 2].__proto__.__proto__ === Object.prototype);  // true

console.log([] instanceof Object);  // true
console.log([] instanceof Array);  // true
  • 任何函数都可以看做是Function “new 出来的”,那我们开一个脑洞:Object也是函数呀,它是不是Function “new 出来的呢”?答案是肯定的 image.png

3. 借用构造函数

  • 为了解决原型中包含引用类型值所带来的的问题和子类构造函数不优雅(重复书写属性)的问题,开发人员通常使用一种叫做“借助构造函数”的技术,也被称为“伪造对象”或“经典继承”
    借用构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是注意使用的是call()绑定上下文
function People(name, sex, age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.arr = [33, 44, 55];  // 注意是复合类型是引用类型值
}
function Student(name, sex, age, school, sid) {
    People.call(this, name, sex, age);  // 也可以使用apply
    this.school = school;
    this.sid = sid;
}

// 此处并没有形成继承关系
var xiaoming = new Student('小明', '男', 12, '中心小学', 100666);
console.log(xiaoming);
xiaoming.arr.push(77);
console.log(xiaoming.arr);
console.log(xiaoming.hasOwnProperty('arr'));

var xiaohong = new Student('小红', '女', 11, '中心小学', 100667);
console.log(xiaohong.arr); // 小红的arr中没有77,完美解决原型中引用类型值带来的问题
console.log(xiaohong.hasOwnProperty('arr));

4、组合继承

  • 将借用原型链继承和借用构造函数的技术组合到一起,叫做组合继承,也叫做伪经典继承
    组合继承是JavaScript中最常用的继承模式
function People(name, sex, age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
}
// 原型链添加方法
People.prototype.sayHello = function () {
    console.log('你好,我是' + this.name + ',今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
    console.log(this.name + '正在睡觉');
}

// 子类 借用构造函数
function Student(name, sex, age, school, sid) {
    People.call(this, name, sex, age);  // 第二次调用超类
    this.school = school;
    this.sid = sid;
}
// 借助原型链实现继承
Student.prototype = new People();  // 第一次调用超类
// 子类添加方法
Student.prototype.exam = function() {
    console.log(this.name + '正在考试');
}
// 覆写父类方法
Student.prototype.sayHello = function() {
    console.log('敬礼,你好,我是' + this.name + ',今年' + this.age + '岁了');
}

var xiaoming = new Student('小明', '男', 12, '中心小学', 100666);
xiaoming.sayHello();
xiaoming.sleep();
xiaoming.exam();

  • 组合继承的缺点:组合继承的最大的问题就是无论什么情况下,都会调用两次超类的构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数的内部

5、原型式继承

  • Object.create(),它可以根据指定的对象为原型创建出新对象

image.png

var obj1 = {
    a: 33,
    b: 45,
    c: 12test: function() {
        console.log(this.a + this.b);
    }
};

var obj2 = Object.create(obj1, {
    d: {
        value: 99
    },
    a: {
        value: 2
    }
});


console.log(obj2.__proto__ === obj1);  // true
console.log(obj2.a);
console.log(obj2.b);
console.log(obj2.c);
console.log(obj2.d);

console.log(obj2.test());  // 47
  • 在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象“类似”的情况下,使用Object.create()即可胜任,称谓原型式继承

  • Object.create()的兼容性写法
    低版本的浏览器不兼容Object.create()

// 道格拉斯·克罗克福德写的一个函数,非常巧妙,面试常考

// 函数的功能是以o为原型,创建新对象
function object(o) {
    // 创建临时构造函数
    function F() {}
    // 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__指向了o
    F.prototype = o;
    // 返回F的实例
    return new F();
}

var obj1 = {
    a: 23,
    b: 5
};

var obj2 = object(obj1);

console.log(obj2.__proto__ === obj1);  // true
console.log(obj2.a);  // 23
console.log(obj2.b);  // 5

6、寄生式继承

  • 寄生式继承:编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法
    image.png
var o1 = {
    name: '小明',
    age: 12,
    sex: '男'
};

var o2 = {
    name: '小红',
    age: 11,
    sex: '女'
};

function f(o) {
    // 以o为原型创建出新对象
    var p = Object.create(o);
    // 补充方法
    p.sayHello = function () {
        console.log('你好,我是' + this.name + '今年' this.age + '岁了');
    }
    p.sleep = function () {
        console.log(this.name + '正在睡觉');
    }
    
    return p;
}

var p1 = f(o1);
p1.sayHello();

var p2 = f(o2);
p2.sayHello();

寄生式继承就是编写一个函数,它可以“增强对象”,只要把对象传入这个函数,这个函数以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式

  • 寄生式继承的缺点

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到prototype上”

7、寄生组合式继承

  • 组合继承缺点:组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数:一次是创建子类原型的时候,另一次是在子类构造函数的内部

  • 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式继承方法

其背后的基本思路是:不必为了指定子类型的原型而调用超类的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

image.png

// 这个函数接收两个参数,subType是子类的构造函数,superType是父类的构造函数
function inheritPrototype(subType, superType) {
	var prototype = Object.create(superType.prototype);
	subType.prototype = prototype;
}

// 父类
function People(name, sex, age) {
	this.name = name;
	this.sex = sex;
	this.age = age;
}
People.prototype.sayHello = function () {
	console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
	console.log(this.name + '正在睡觉');
}

// 子类
function Student(name, sex, age, school, sid) {
	// 借助构造函数
	People.call(this, name, sex, age);
	this.school = school;
	this.sid = sid;
}
// 调用我们自己编写的inheritPrototype函数,这个函数可以让Student类的prototype指向“以People.prototype为原型的一个新对象”
inheritPrototype(Student, People);
 
Student.prototype.exam = function() {
	console.log(this.name + '正在考试');
};
Student.prototype.sayHello = function() {
	console.log('敬礼!你好,我是' + this.name + '今年' + this.age + '岁了,我是' + this.school + '学校的学生');
};

var xiaoming = new Student('小明', '男', 12, '史莱克学院', 100666);
xiaoming.sleep();
xiaoming.exam();
xiaoming.sayHello();

8、instanceof运算符

  • instanceof运算符用来检测“某对象是不是某个类的实例”,比如:xiaoming instanceof Student

  • 底层机理:检查Student.prototype属性是否在xiaoming的原型链上(多少层都行,只要在就行)