• JS基础-构造函数、原型、原型链、继承


    1.构造函数与原型

    1.1 概述

    在典型的OOP的语言中(如JAVA),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS并没有引入类的概念。

    面向对象程序设计(Object Oriented Programming)

    ES6,全称ECMAScript6.0,2015.06发版。但是目前浏览器的 JavaScript 是 ES5 版本,大多数高版本的浏览器也支持ES6,不过只实现了ES6的部分特性和功能。

    在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。

    创建对象可以通过以下几种方式:

    • 对象字面量
    • new Object()---也就是---new 操作符 + Object 创建对象
    • 自定义构造函数
    • Object.create

    1.2 构造函数

    构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总是与new一起使用。
    我们可以把对象中的一些公共的属性和方法抽取出来,然后封装到这个函数里面。

    在JS中,使用构造函数时要注意以下两点:

    1. 构造函数用于创建某一类对象,其首字母要大写
    2. 构造函数要和new一起使用才有意义

    创建对象可以通过以下几种方式:

    1. 利用 对象字面量创建对象
    var obj1 = {
    	uname: 'ldh',
    	age: 20
    };
    console.log("obj1:", obj1);
    /* 
     {uname: "ldh", age: 20}
     age:20
     uname:"ldh"
     __proto__:Object
     */
    
    var o1 = {
    	name: "o1"
    };
    
    /* var person = {
    	name: "lisi",
    	age: 21,
    	family: ["lida", "lier", "wangwu"],
    	say: function() {
    		alert(this.name);
    	}
    } */
    
    1. 利用 new Object() 创建对象
    var obj2 = new Object();
    obj2.uname = 'ldh';
    obj2.age = 20;
    console.log("obj2:", obj2);
    /* 
     {uname: "ldh", age: 20}
     age:20
     uname:"ldh"
     __proto__:Object
     */
    
    var o2 = new Object({
    	name: "o2happy"
    });
    console.log("o2:", o2);
    /* 
     {name: "o2happy"}
     name:"o2happy"
     __proto__:Object
     */
    /* var person = new Object();
        person.name = "lisi";
        person.age = 21;
        person.family = ["lida","lier","wangwu"];
        person.say = function(){
           alert(this.name);
        } */		
    
    1. 利用构造函数创建对象
    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    	this.sing = function() {
    		console.log("我会唱歌");
    	}
    }
    ldh = new Star();
    ldh.sing();
    zxy = new Star('张学友', 20);
    console.log("zxy:", zxy);
    /* 
     我会唱歌
     Star {uname: "张学友", age: 20, sing: ƒ}
     age:20
     sing:ƒ ()
     uname:"张学友"
     __proto__:Object
     */
    
    1. Object.create
    var p = {
    	name: "p"
    };
    var o4 = Object.create(p);
    console.log(o4);
    /* 
     {}
     __proto__: Object
     
     控制台输入
     o4.__proto__ === p;
     true
     */
    

    这里需要注意,用Object.create创建出的o4 是一个空对象,p 是 o4 的原型对象,可以通过原型链来找到name属性。

    javascript的构造函数中可以添加一些成员

    • 静态成员---在构造函数本身上添加的成员,只能由构造函数本身来访问
    • 实例成员---在构造函数内部的this上添加的成员,只能由实例化的对象来访问
    // 构造函数中的属性和方法称为成员,成员可以添加
    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    	this.sing = function() {
    		console.log("我会唱歌");
    	}
    }
    var ldh = new Star('刘德华', 20);
    // 1.实例成员 就是构造函数内部通过this添加的成员 uname age sing 就是实例成员
    // 实例成员只能通过实例化的对象来访问
    console.log(ldh.uname);
    ldh.sing();
    // 不可以通过构造函数来访问实例成员
    console.log(Star.uname) // 返回undefined
    
    // 2.静态成员 在构造函数本身上添加的成员 sex 就是静态成员
    Star.sex = '男';
    // 静态成员智能通过构造函数来访问
    console.log(Star.sex);
    // 不可以通过对象来访问静态成员
    console.log(ldh.sex); // 返回undefined
    

    1.3 构造函数的问题

    构造函数方法很好用,但是存在浪费内存的问题

    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    	this.sing = function() {
    		console.log("我会唱歌");
    	}
    }
    var ldh = new Star('刘德华', 19);
    var zxy = new Star('张学友', 20);
    
    /* 当创建实例对象的时候
    var ldh = new Star('刘德华', 19);
    var zxy = new Star('张学友', 20);
    对于简单的属性, 简单的数据类型, 直接赋值就可以
    this.uname = uname;
    this.age = age;
    
    但对于复杂数据类型, 比如函数
    this.sing = function() {
    	console.log("我会唱歌");
    }
    在创建实例对象的时候
    var ldh = new Star('刘德华', 19);
    var zxy = new Star('张学友', 20);
    ldh
    zxy
    都会单独去开辟内存空间去存放
    function() {}
    即不同地址存放 */
    
    console.log(ldh.sing === zxy.sing); // 返回 false
    

    console.log(ldh.sing === zxy.sing); // 返回 false

    1.4 构造函数 原型对象 prototype

    构造函数通过原型分配的函数是所有对象所共享

    JavaScript规定,每一个构造函数都有一个prototype,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。

    我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。

    问答

    1. 原型是什么?
      一个对象,也称 prototype 为原型对象
    2. 原型的作用是什么?
      共享方法
    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    	/* this.sing = function() {
    		console.log("我会唱歌");
    	} */
    }
    Star.prototype.sing = function() {
    	console.log("我会唱歌");
    }
    var ldh = new Star('刘德华', 20);
    var zxy = new Star('张学友', 22);
    console.dir(Star);
    ldh.sing();
    zxy.sing();
    // 2.一般情况下,公共属性定义到构造函数里,公共的方法放到原型对象上
    console.log(ldh.sing === zxy.sing); // 返回 true			
    

    1.5 对象原型 __proto__

    对象都会有一个属性 __proto__ ,指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype原型对象的属性和方法,就是因为对象有 __proto__ 原型的存在。

    __proto__ 对象原型和原型对象 prototype 是等价的

    __proto__ 对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条线路,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    }
    Star.prototype.sing = function() {
    	console.log("我会唱歌");
    }
    var ldh = new Star('刘德华', 20);
    var zxy = new Star('张学友', 22);
    ldh.sing();
    console.dir(Star);
    console.log(ldh); // 对象身上系统自己添加一个__proto__指向构造函数的原型对象prototype
    
    console.log(ldh.__proto__ === Star.prototype) // 返回true
    // 方法的查找规则:首先先看 ldh 对象身上是否有 sing 方法,如果有就执行这个对象上的sing
    // 如果没有sing这个方法,因为有__proto__ 的存在,就去构造函数原型对象prototype身上去查找sing这个方法
    

    1.6 constructor 构造函数

    对象原型(__proto__)和构造函数( prototype )原型对象里面都有一个属性 constructor 属性

    constructor 我们称为构造函数,因为它指回构造函数本身

    [可以查看对应代码]

    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    }
    // 很多情况下,我们需要手动利用constructor 这个属性指回 原来的构造函数
    /* Star.prototype.sing = function() {
    	console.log("我会唱歌");
    }
    Star.prototype.movie = function() {
    	console.log("我会演电影");
    } */
    Star.prototype = {
    	// 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
    	constructor: Star,
    	sing: function() {
    		console.log("我会唱歌");
    	},
    	movie: function() {
    		console.log("我会演电影");
    	}
    }
    var ldh = new Star('刘德华', 20);
    var zxy = new Star('张学友', 22);
    console.log(Star.prototype);
    console.log(ldh.__proto__);
    console.log(Star.prototype.constructor);
    console.log(ldh.__proto__.constructor);
    

    1.7 构造函数、实例、原型对象三者之间的关系

    三者关系

    1.8 原型链

    原型链

    1.9 JavaScript的成员查找机制(规则)

    1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
    2. 如果没有就查找它的原型(也就是__proto__ 指向的prototype原型对象)
    3. 如果还没有,就查找原型对象的原型(Object的原型对象)
    4. 依此类推一直找到Object为止(null)
    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    }
    Star.prototype.sing = function() {
    	console.log('我会唱歌');
    }
    var ldh = new Star('刘德华', 20);
    ldh.sex = '男';
    console.log(ldh.sex);
    
    Star.prototype.color = 'pink';
    console.log(ldh.color);
    
    Object.prototype.feeling = 'good';
    console.log(ldh.feeling);
    
    console.log(ldh.cat);
    
    console.log(Object.prototype); //有toString方法
    console.log(Star.prototype);
    

    1.10 原型对象 this 指向

    谁调用,this就指向谁

    function Star(uname, age) {
    	this.uname = uname;
    	this.age = age;
    }
    var that;
    Star.prototype.sing = function() {
    	console.log('我会唱歌');
    	that = this;
    }
    var ldh = new Star('刘德华', 18);
    // 1.在构造函数中,里面this指向的是对象实例ldh
    ldh.sing();
    console.log(that === ldh); // 返回true
    
    // 2.原型对象函数里面的 this 指向的是 实例对象 ldh
    

    1.11 扩展内置对象

    可以通过原型对象,对原来的内置对象进行扩展自定义的方法。

    比如给数组增加自定义求和的功能。

    注意:数组和字符串对象不能给原型对象覆盖操作 Array.prototype = {}, 只能是 Array.prototype.xxx = function(){}

    console.log(Array.prototype);
    Array.prototype.sum = function() {
    	var sum = 0;
    	for (var i = 0; i < this.length; i++) {
    		sum += this[i];
    	}
    	return sum;
    }
    var arr = [1, 2, 3];
    console.log(arr.sum());
    console.log(Array.prototype);
    
    var arr1 = new Array(11, 22, 33);
    console.log(arr1.sum());			
    
    Array.prototype = {
    	sum: function() {
    		var sum = 0;
    		for (var i = 0; i < this.length; i++) {
    			sum += this[i];
    		}
    		return sum;
    	}
    }
    var arr = [1, 2, 3];
    console.log(arr.sum());
    console.log(Array.prototype);
    
    var arr1 = new Array(11, 22, 33);
    console.log(arr1.sum());
    

    这样会报错
    Uncaught TypeError: arr.sum is not a function at 09 扩展内置对象方法.html:26

    2.继承

    ES6之前并没有提供 extends 继承。

    可以通过 构造函数+原型对象 模拟实现继承,被称为 组合继承。

    function Father(name, age) {
    	this.name = name;
    	this.age = age
    }
    Father.prototype.say = function() {
    	console.log('I have lots of experience.')
    }
    

    继承

    2.1 借助call

    function Son(name, age, score) {
    	Father.call(this, name, age);
    	this.score = score;
    }
    

    问题:只能实现部分继承,没有继承到父类原型对象上的方法。

    2.2 借助原型链

    function Son(score) {
    	this.score = score
    }
    Son.prototype = new Father();
    let s1 = new Son();
    let s2 = new Son();
    
    console.log(s1.__proto__ === s2.__proto__); // true
    

    这样就可以拿到父类原型对象上的方法。

    问题:s1、s2 原型对象是公用的,s1 改变会让s2 发生改变.

    2.3 1+2 前两种组合

    function Son(name, age, score) {
    	Father.call(this, name, age);
    	this.score = score;
    }
    Son.prototype = new Father();
    // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的原型对象 
    Son.prototype.constructor = Son;
    

    问题:在每次生成 Son 实例 s1、s2 的时候,都会执行 Father 的构造函数。

    2.4 Son.prototype = Father.prototype

    function Son(name, age, score) {
    	Father.call(this, name, age);
    	this.score = score;
    }
    Son.prototype = Father.prototype;
    // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的原型对象 
    Son.prototype.constructor = Son;
    

    这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问.

    问题:由于它们共用一个原型对象,当修改子原型对象的时候,父原型对象也会改变。

    2.5 推荐使用(寄生组合继承)

    function Son(name, age, score) {
    	Father.call(this, name, age);
    	this.score = score;
    }
    Son.prototype = Object.create(Father.prototype);
    Son.prototype.constructor = Son;
    

    2.6 ES6 class

    class Father {
    	constructor(name, age) {
    		this.name = name;
    		this.age = age
    	}
    	say() {
    		console.log('I have lots of experience.')
    	}
    }
    
    class Son extends Father {
    	constructor(name, age, score) {
    		super(name, age);
    		this.score = score
    	}
    	test() {
    		console.log('I love test.')
    	}
    }
    let cherry = new Son('lin', 18, 99)
    

    常考面试题

    1.请描述一下原型链

    • 原型链就是多个对象通过 __proto__ 的方式连接了起来。
    • 实例对象.__proto__ 指向原型对象,这个对象也有 __proto__ 属性,指向 Object.prototype;它也有 __proto__ 属性,指向 null.

    构造函数与原型对象的关系:

    • 构造函数有 prototype 属性,指向原型对象
    • 当寻找一个对象是否具备某属性/方法时,沿着原型链向上查找。

    具体查找机制(规则):

    1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
    2. 如果没有就查找它的原型(也就是__proto__ 指向的prototype原型对象)
    3. 如果还没有,就查找原型对象的原型(Object的原型对象)
    4. 依此类推一直找到Object为止(null)

    举例子

    function Star(name, age) {
    	this.name = name;
    	this.age = age
    }
    let lin = new Star('lin', 18);
    

    原型链

    2.实现 new 操作符

    • 创建一个新的空对象
    • 使空对象的__proto__指向构造函数的原型(prototype)
    • 把this绑定到空对象
    • 执行构造函数,为空对象添加属性
    • 判断函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
    • --如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用。
    function myNew(Con, ...args){
        let obj = {}
        obj.__proto__ = Con.prototype
        let result = Con.call(obj, ...args)
        return result instanceof Object ? result : obj
    }
    let lin = myNew(Star,'lin',18)
    // 相当于
    let lin = new Star('lin',18)
    

    3.如何进行原型链的判断

    object instanceof constructor

    • object某个实例对象, constructor某个构造函数
    • instanceof 运算符用来检测 某个构造函数.prototype 是否存在于参数 某个实例对象 的原型链上。
      instanceof

    4.实现 instanceof 操作符

    function myInstanceof(left, right) {
    	//基本数据类型直接返回false
    	if (typeof left !== 'object' || left == null) return false;
    	//getPrototypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    	let proto = Object.getPrototypeOf(left);
    	while (true) {
    		// 如果查找到尽头,还没找到,return false
    		if (proto == null) return false;
    		//找到相同的原型对象
    		if (proto === right.prototype) return true;
    		proto = Object.getPrototypeOf(proto);
    	}
    }
    
    console.log(myInstanceof("111", String)); //false
    console.log(myInstanceof(new String("111"), String)); //true
    

    5.ES6之前如何实现继承

    见第2部分的内容

  • 相关阅读:
    基础总结深入:数据类型的分类和判断(数据、内存、变量) 对象 函数 回调函数 IIFE 函数中的this 分号
    BOM 定时器 通过修改元素的类来改变css JSON
    事件 事件的冒泡 事件的委派 事件的绑定 事件的传播
    DOM修改 使用DOM操作CSS
    包装类 Date Math 字符串的相关的方法 正则表达式 DOM DOM查询
    数组 call()、apply()、bind()的使用 this arguments
    autocad 二次开发 最小包围圆算法
    win10 objectarx向导在 vs2015中不起作用的解决办法
    AutoCad 二次开发 jig操作之标注跟随线移动
    AutoCad 二次开发 文字镜像
  • 原文地址:https://www.cnblogs.com/chrislinlin/p/12682330.html
Copyright © 2020-2023  润新知