js之对象、构造函数及面向对象编程

221 阅读31分钟

对象

属性的类型

属性分为两种:数据属性和访问器属性

  1. 数据属性

    [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改成访问器属性。

    [[Enumerable]]:属性是否可以使用for-in枚举。

    [[Writable]]:表示属性是否可以被修改。

    [[Value]]:包含属性的原始值

    let obj={};
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'
    })
    
  2. 访问器属性

    访问器属性不包含数据值,他们包含一个获取函数和一个设置函数。在读取访问器是,会调用获取函数,在写入访问器时会调用设置函数。

    [[Get]]:获取函数,在读取属性调用时调用。

    [[Set]]:设置函数,在写入属性时调用。

    let obj={
        '_name':'atuotuo'
    };
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'get(){
        	return this._name
    	},
        set(newv){
            this._name = newv;
        }
    })
    

Object类的其他方法

  1. Object.getOwnPorpertyDescroptor()可以获取指定属性的属性描述符

  2. Object.assign(newObj,oldObj) 合并对象,把源对象所有的本地属性一起赋值到目标对象上,对每个对象的拷贝是浅拷贝。

  3. Object.is() 判断两个对象是否相等。

    更多Object的方法和属性请参考:developer.mozilla.org/zh-CN/docs/…

创建对象

工厂模式

工厂模式是用于抽象创建特定对象的过程

function createPerson(name,age)
{
    return {
        name:name,
        age:age
        say(){
            console.log(this.name,this.age);
        }
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);

这种模式可以解决创建多个类似对象的问题,但是没有解决对象表示问题。

构造函数模式

构造函数时用于创建特定类型对象。

构造函数与不同函数的区别就是调用方式不同,任何函数只要使用new操作符调用就是构造函数,而不使用new操作调用的函数就是普通函数。

function Person(name,age)
{
    this.name = name;
    this.age = age;
    this.say = function(){
    	console.log(this.name,this.age);
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // false

要创建Person的实例,应使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 在创建新对象内部设置__proto__隐式原型指向构造函数的prototype显示原型。
  3. 构造函数内部的this指向这个新创建的对象
  4. 执行构造函数内部的代码。
  5. 如果构造函数返回非空对象,则返回该对象,否则返回创建的新对象。

构造函数的问题

构造函数的主要问题在于:其定义的方法会在每个实例上都创建一遍,其实他们做的都是一样的事情,没有必要创建多次。那么就需要找到一个构造函数本身的全局属性来创建每个对象都能放到的属性,将方法定义到这个属性上,并且这个属性可以访问到对象的属性。这个属性就是原型(prototype)。

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。是用原型对象的好处是:在它上面定义的属性和方法可以被对象实例共享。

function Person(name,age)
{
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log(this.name);
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // true

继承

原型链继承

原型链继承的基本思想:通过原型继承多个引用类型的属性和方法。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score){
    this.score = score;
}
Student.prototype = new Person('atuotuo'); // 继承

let s1 = new Student(12);
s1.hobby.push('3');
let s2 = new Student(13);
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2,3

存在问题:

1. 原型中包含引用类型值的时候,原型中包含的引用值会在所有实例间共享。
1. 子类在实例化时不能给父类型的构造函数传参数。

构造函数继承

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score,name){
    Person.call(this,name); // 继承
    this.score = score;
}

let s1 = new Student(12,'tuotuo');
s1.hobby.push('3');
let s2 = new Student(13'zs');
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:必须在构造函数中定义方法,原型上的方法没有继承

组合继承(原型链 + 构造函数)

组合继承综合了原型链和构造函数,将两者的有点集中起来。基本思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = new Person() //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:父类被创建两次,而且在子的原型上还会出现父类的实例属性,会造成资源浪费。

原型式继承

原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,但是原型式继承与原型链继承类似,属性中包含引用值始终会共享内存。

function object(o){
	function F(){};
	F.prototype = o;
	return new F();
}

寄生式继承

寄生式继承其实就是使用一个工厂函数对原型式继承进行封装,可以在新创建的对象添加其他属性和方法。

function object(o){
	function F(){};
	F.prototype = o;
	let obj = new F();
	obj.say = function(){
		console.log(1);
	}
	return obj;
}

寄生式组合继承

主要是解决组合继承的问题:父类构造函数始终会被调用两次。

基本思路:不通过调用父类的构造函数给子类原型赋值,而是取得父类原型的创建一个空对象然后赋值子类原型。就是使用寄生式继承来继承父类原型。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = Object.create(Person.prototype); //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

类(class)是ES6新的基础性语法糖结构,表面上看是可以支持的面向对象变量,但实际上背后使用的仍然是原型和构造函数的概念。

类定义

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

class Person{}

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数

class Person
{
	constructor(name){
		this.name = name;
	}
}

实例、原型和类成员

class Person
{
	constructor(name){
        // 在构造函数内部,可以通过this定义实例成员
		this._name = name;
	}
    //在类块中定义的所有内容都会定义在类的原型上
    say(){
        console.log(this.name);
    }
    // 通过get 和 set设置获取和设置访问器
    get name(){
        return this._name;
    }
    
    set name(newName){
        this._name = newName;
    }
    // 静态方法,实际上就是定义在类上的方法
    static speak(){
        console.log('speak');
    }
}

继承

基础继承

使用extends关键字,可以单继承

class Person
{

}
class Student extends Person
{

}

调用父类构造函数和静态类

派生类的方法可以通过super关键字引用他们的原型。

class Person
{
	static speak(){
	
	}
}
class Student extends Person
{
	constructor(){
		super() // 在派生类的构造函数中使用super可以调用父类的构造函数
	}
    static speak(){
        super.speak() //在静态方法中可以通过super调用继承的类上的定义的静态方法
    }
}

注意点:

super只能在派生的构造函数和静态方法中使用

不能但是引用super关键字,要么它调用构造函数,要么引用静态方法

调用super() 会调用父类构造函数,并将返回的实例赋给this

在类构造函数中,不能再调用super()之前引用this。

抽象基类

抽象基类:可供其他类继承,但本身不会被实例化,es没有这种专门的语法,但是通过new.target很容易实现。new.target保证通过new关键字调用的类和函数。

// 抽象基类
class Person{
    constructor(){
        if(new.target === Person){
            throw new Error("该类为抽象类,不能直接实例化");
        }
    }
}

常见面试题

new操作符的实现原理

  1. 首先创建一个新的对象

  2. 设置原型,将对象的隐式原型设置为构造函数的 prototype 对象

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

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

    实现

    function objectFactory(){
        let newobject = null;
       	let constructor = Array.prototype.shift.call(arguments);
        
        let result = null;
        if(typeof constructor !== 'function'){
            return;
        }
        
        // 新建对象,让对象的原型为构造函数的原型对象
        newobject = Object.create(constructor.prototype);
        // 将新创建的对象作为,构造函数的this对象
        result = constructor.apply(newobject,arguments);
        
        // 判断返回值的结果
        let flag = result && (typeof result === "object" || typeof result == 'function');
        
        return flag ? result:newobject;
    }
    

对原型和原型链的理解

原型对象:每个构造函数内部都有一个prototype属性(原型对象),这个对象包含了该构造函数的所有实例共享的属性和方法,prototype.constructor 指向该对象。

隐式原型:当使用构造函数创建一个对象时,在这个对象内部将创建一个指针(__proto),指向原型对象。一般来说不可直接访问__proto,ES5新增一个方法可以访问,Object.getPrototypeOf(obj).

原型链:在js中万物皆对象,每个对象那个中都有指针__proto__,当访问一个对象中的属性时,如果对象内部不存在这个属性,则会再原型中寻找,如果原型中还没有,就去原型的原型中找,原型链的尽头是Object.prototype。

img转存失败,建议直接上传图片文件

原型链的终点是什么

由于object是构造函数,原型中点是Object.prototype.__proto__,而 Object.prototype.__proto__ = null。所以原型链的重点为null。

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

使用 obj.hasOwnProperty() 方法来判断属性是否属于原型链上的属性

instanceOf的原理及实现

instanceof是通过原型链来实现的,通过检测原型链中是否存在该类对象的原型相同

function instanceOf(obj,cla)
{
    let proto = Object.getPrototypeOf(obj);

    while(proto)
    {
        if(proto == cla.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

对象

属性的类型

属性分为两种:数据属性和访问器属性

  1. 数据属性

    [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改成访问器属性。

    [[Enumerable]]:属性是否可以使用for-in枚举。

    [[Writable]]:表示属性是否可以被修改。

    [[Value]]:包含属性的原始值

    let obj={};
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'
    })
    
  2. 访问器属性

    访问器属性不包含数据值,他们包含一个获取函数和一个设置函数。在读取访问器是,会调用获取函数,在写入访问器时会调用设置函数。

    [[Get]]:获取函数,在读取属性调用时调用。

    [[Set]]:设置函数,在写入属性时调用。

    let obj={
        '_name':'atuotuo'
    };
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'get(){
        	return this._name
    	},
        set(newv){
            this._name = newv;
        }
    })
    

Object类的其他方法

  1. Object.getOwnPorpertyDescroptor()可以获取指定属性的属性描述符

  2. Object.assign(newObj,oldObj) 合并对象,把源对象所有的本地属性一起赋值到目标对象上,对每个对象的拷贝是浅拷贝。

  3. Object.is() 判断两个对象是否相等。

    更多Object的方法和属性请参考:developer.mozilla.org/zh-CN/docs/…

创建对象

工厂模式

工厂模式是用于抽象创建特定对象的过程

function createPerson(name,age)
{
    return {
        name:name,
        age:age
        say(){
            console.log(this.name,this.age);
        }
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);

这种模式可以解决创建多个类似对象的问题,但是没有解决对象表示问题。

构造函数模式

构造函数时用于创建特定类型对象。

构造函数与不同函数的区别就是调用方式不同,任何函数只要使用new操作符调用就是构造函数,而不使用new操作调用的函数就是普通函数。

function Person(name,age)
{
    this.name = name;
    this.age = age;
    this.say = function(){
    	console.log(this.name,this.age);
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // false

要创建Person的实例,应使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 在创建新对象内部设置__proto__隐式原型指向构造函数的prototype显示原型。
  3. 构造函数内部的this指向这个新创建的对象
  4. 执行构造函数内部的代码。
  5. 如果构造函数返回非空对象,则返回该对象,否则返回创建的新对象。

构造函数的问题

构造函数的主要问题在于:其定义的方法会在每个实例上都创建一遍,其实他们做的都是一样的事情,没有必要创建多次。那么就需要找到一个构造函数本身的全局属性来创建每个对象都能放到的属性,将方法定义到这个属性上,并且这个属性可以访问到对象的属性。这个属性就是原型(prototype)。

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。是用原型对象的好处是:在它上面定义的属性和方法可以被对象实例共享。

function Person(name,age)
{
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log(this.name);
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // true

继承

原型链继承

原型链继承的基本思想:通过原型继承多个引用类型的属性和方法。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score){
    this.score = score;
}
Student.prototype = new Person('atuotuo'); // 继承

let s1 = new Student(12);
s1.hobby.push('3');
let s2 = new Student(13);
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2,3

存在问题:

1. 原型中包含引用类型值的时候,原型中包含的引用值会在所有实例间共享。
1. 子类在实例化时不能给父类型的构造函数传参数。

构造函数继承

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score,name){
    Person.call(this,name); // 继承
    this.score = score;
}

let s1 = new Student(12,'tuotuo');
s1.hobby.push('3');
let s2 = new Student(13'zs');
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:必须在构造函数中定义方法,原型上的方法没有继承

组合继承(原型链 + 构造函数)

组合继承综合了原型链和构造函数,将两者的有点集中起来。基本思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = new Person() //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:父类被创建两次,而且在子的原型上还会出现父类的实例属性,会造成资源浪费。

原型式继承

原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,但是原型式继承与原型链继承类似,属性中包含引用值始终会共享内存。

function object(o){
	function F(){};
	F.prototype = o;
	return new F();
}

寄生式继承

寄生式继承其实就是使用一个工厂函数对原型式继承进行封装,可以在新创建的对象添加其他属性和方法。

function object(o){
	function F(){};
	F.prototype = o;
	let obj = new F();
	obj.say = function(){
		console.log(1);
	}
	return obj;
}

寄生式组合继承

主要是解决组合继承的问题:父类构造函数始终会被调用两次。

基本思路:不通过调用父类的构造函数给子类原型赋值,而是取得父类原型的创建一个空对象然后赋值子类原型。就是使用寄生式继承来继承父类原型。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = Object.create(Person.prototype); //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

类(class)是ES6新的基础性语法糖结构,表面上看是可以支持的面向对象变量,但实际上背后使用的仍然是原型和构造函数的概念。

类定义

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

class Person{}

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数

class Person
{
	constructor(name){
		this.name = name;
	}
}

实例、原型和类成员

class Person
{
	constructor(name){
        // 在构造函数内部,可以通过this定义实例成员
		this.name = name;
	}
    
    say(){
        console.log(this.name);
    }
}

对象

属性的类型

属性分为两种:数据属性和访问器属性

  1. 数据属性

    [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改成访问器属性。

    [[Enumerable]]:属性是否可以使用for-in枚举。

    [[Writable]]:表示属性是否可以被修改。

    [[Value]]:包含属性的原始值

    let obj={};
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'
    })
    
  2. 访问器属性

    访问器属性不包含数据值,他们包含一个获取函数和一个设置函数。在读取访问器是,会调用获取函数,在写入访问器时会调用设置函数。

    [[Get]]:获取函数,在读取属性调用时调用。

    [[Set]]:设置函数,在写入属性时调用。

    let obj={
        '_name':'atuotuo'
    };
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'get(){
        	return this._name
    	},
        set(newv){
            this._name = newv;
        }
    })
    

Object类的其他方法

  1. Object.getOwnPorpertyDescroptor()可以获取指定属性的属性描述符

  2. Object.assign(newObj,oldObj) 合并对象,把源对象所有的本地属性一起赋值到目标对象上,对每个对象的拷贝是浅拷贝。

  3. Object.is() 判断两个对象是否相等。

    更多Object的方法和属性请参考:developer.mozilla.org/zh-CN/docs/…

创建对象

工厂模式

工厂模式是用于抽象创建特定对象的过程

function createPerson(name,age)
{
    return {
        name:name,
        age:age
        say(){
            console.log(this.name,this.age);
        }
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);

这种模式可以解决创建多个类似对象的问题,但是没有解决对象表示问题。

构造函数模式

构造函数时用于创建特定类型对象。

构造函数与不同函数的区别就是调用方式不同,任何函数只要使用new操作符调用就是构造函数,而不使用new操作调用的函数就是普通函数。

function Person(name,age)
{
    this.name = name;
    this.age = age;
    this.say = function(){
    	console.log(this.name,this.age);
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // false

要创建Person的实例,应使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 在创建新对象内部设置__proto__隐式原型指向构造函数的prototype显示原型。
  3. 构造函数内部的this指向这个新创建的对象
  4. 执行构造函数内部的代码。
  5. 如果构造函数返回非空对象,则返回该对象,否则返回创建的新对象。

构造函数的问题

构造函数的主要问题在于:其定义的方法会在每个实例上都创建一遍,其实他们做的都是一样的事情,没有必要创建多次。那么就需要找到一个构造函数本身的全局属性来创建每个对象都能放到的属性,将方法定义到这个属性上,并且这个属性可以访问到对象的属性。这个属性就是原型(prototype)。

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。是用原型对象的好处是:在它上面定义的属性和方法可以被对象实例共享。

function Person(name,age)
{
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log(this.name);
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // true

继承

原型链继承

原型链继承的基本思想:通过原型继承多个引用类型的属性和方法。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score){
    this.score = score;
}
Student.prototype = new Person('atuotuo'); // 继承

let s1 = new Student(12);
s1.hobby.push('3');
let s2 = new Student(13);
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2,3

存在问题:

1. 原型中包含引用类型值的时候,原型中包含的引用值会在所有实例间共享。
1. 子类在实例化时不能给父类型的构造函数传参数。

构造函数继承

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score,name){
    Person.call(this,name); // 继承
    this.score = score;
}

let s1 = new Student(12,'tuotuo');
s1.hobby.push('3');
let s2 = new Student(13'zs');
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:必须在构造函数中定义方法,原型上的方法没有继承

组合继承(原型链 + 构造函数)

组合继承综合了原型链和构造函数,将两者的有点集中起来。基本思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = new Person() //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:父类被创建两次,而且在子的原型上还会出现父类的实例属性,会造成资源浪费。

原型式继承

原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,但是原型式继承与原型链继承类似,属性中包含引用值始终会共享内存。

function object(o){
	function F(){};
	F.prototype = o;
	return new F();
}

寄生式继承

寄生式继承其实就是使用一个工厂函数对原型式继承进行封装,可以在新创建的对象添加其他属性和方法。

function object(o){
	function F(){};
	F.prototype = o;
	let obj = new F();
	obj.say = function(){
		console.log(1);
	}
	return obj;
}

寄生式组合继承

主要是解决组合继承的问题:父类构造函数始终会被调用两次。

基本思路:不通过调用父类的构造函数给子类原型赋值,而是取得父类原型的创建一个空对象然后赋值子类原型。就是使用寄生式继承来继承父类原型。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = Object.create(Person.prototype); //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

类(class)是ES6新的基础性语法糖结构,表面上看是可以支持的面向对象变量,但实际上背后使用的仍然是原型和构造函数的概念。

类定义

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

class Person{}

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数

class Person
{
	constructor(name){
		this.name = name;
	}
}

实例、原型和类成员

class Person
{
	constructor(name){
        // 在构造函数内部,可以通过this定义实例成员
		this._name = name;
	}
    //在类块中定义的所有内容都会定义在类的原型上
    say(){
        console.log(this.name);
    }
    // 通过get 和 set设置获取和设置访问器
    get name(){
        return this._name;
    }
    
    set name(newName){
        this._name = newName;
    }
    // 静态方法,实际上就是定义在类上的方法
    static speak(){
        console.log('speak');
    }
}

继承

基础继承

使用extends关键字,可以单继承

class Person
{

}
class Student extends Person
{

}

调用父类构造函数和静态类

派生类的方法可以通过super关键字引用他们的原型。

class Person
{
	static speak(){
	
	}
}
class Student extends Person
{
	constructor(){
		super() // 在派生类的构造函数中使用super可以调用父类的构造函数
	}
    static speak(){
        super.speak() //在静态方法中可以通过super调用继承的类上的定义的静态方法
    }
}

注意点:

super只能在派生的构造函数和静态方法中使用

不能但是引用super关键字,要么它调用构造函数,要么引用静态方法

调用super() 会调用父类构造函数,并将返回的实例赋给this

在类构造函数中,不能再调用super()之前引用this。

抽象基类

抽象基类:可供其他类继承,但本身不会被实例化,es没有这种专门的语法,但是通过new.target很容易实现。new.target保证通过new关键字调用的类和函数。

// 抽象基类
class Person{
    constructor(){
        if(new.target === Person){
            throw new Error("该类为抽象类,不能直接实例化");
        }
    }
}

常见面试题

new操作符的实现原理

  1. 首先创建一个新的对象

  2. 设置原型,将对象的隐式原型设置为构造函数的 prototype 对象

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

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

    实现

    function objectFactory(){
        let newobject = null;
       	let constructor = Array.prototype.shift.call(arguments);
        
        let result = null;
        if(typeof constructor !== 'function'){
            return;
        }
        
        // 新建对象,让对象的原型为构造函数的原型对象
        newobject = Object.create(constructor.prototype);
        // 将新创建的对象作为,构造函数的this对象
        result = constructor.apply(newobject,arguments);
        
        // 判断返回值的结果
        let flag = result && (typeof result === "object" || typeof result == 'function');
        
        return flag ? result:newobject;
    }
    

对原型和原型链的理解

原型对象:每个构造函数内部都有一个prototype属性(原型对象),这个对象包含了该构造函数的所有实例共享的属性和方法,prototype.constructor 指向该对象。

隐式原型:当使用构造函数创建一个对象时,在这个对象内部将创建一个指针(__proto),指向原型对象。一般来说不可直接访问__proto,ES5新增一个方法可以访问,Object.getPrototypeOf(obj).

原型链:在js中万物皆对象,每个对象那个中都有指针__proto__,当访问一个对象中的属性时,如果对象内部不存在这个属性,则会再原型中寻找,如果原型中还没有,就去原型的原型中找,原型链的尽头是Object.prototype。

img转存失败,建议直接上传图片文件

原型链的终点是什么

由于object是构造函数,原型中点是Object.prototype.__proto__,而 Object.prototype.__proto__ = null。所以原型链的重点为null。

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

使用 obj.hasOwnProperty() 方法来判断属性是否属于原型链上的属性

instanceOf的原理及实现

instanceof是通过原型链来实现的,通过检测原型链中是否存在该类对象的原型相同

function instanceOf(obj,cla)
{
    let proto = Object.getPrototypeOf(obj);

    while(proto)
    {
        if(proto == cla.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

对象

属性的类型

属性分为两种:数据属性和访问器属性

  1. 数据属性

    [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改成访问器属性。

    [[Enumerable]]:属性是否可以使用for-in枚举。

    [[Writable]]:表示属性是否可以被修改。

    [[Value]]:包含属性的原始值

    let obj={};
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'
    })
    
  2. 访问器属性

    访问器属性不包含数据值,他们包含一个获取函数和一个设置函数。在读取访问器是,会调用获取函数,在写入访问器时会调用设置函数。

    [[Get]]:获取函数,在读取属性调用时调用。

    [[Set]]:设置函数,在写入属性时调用。

    let obj={
        '_name':'atuotuo'
    };
    Object.defineProperty(obj,"name",{
        configurable:true,
        writable:true,
        enumerable:true,
        value:'atuotuo'get(){
        	return this._name
    	},
        set(newv){
            this._name = newv;
        }
    })
    

Object类的其他方法

  1. Object.getOwnPorpertyDescroptor()可以获取指定属性的属性描述符

  2. Object.assign(newObj,oldObj) 合并对象,把源对象所有的本地属性一起赋值到目标对象上,对每个对象的拷贝是浅拷贝。

  3. Object.is() 判断两个对象是否相等。

    更多Object的方法和属性请参考:developer.mozilla.org/zh-CN/docs/…

创建对象

工厂模式

工厂模式是用于抽象创建特定对象的过程

function createPerson(name,age)
{
    return {
        name:name,
        age:age
        say(){
            console.log(this.name,this.age);
        }
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);

这种模式可以解决创建多个类似对象的问题,但是没有解决对象表示问题。

构造函数模式

构造函数时用于创建特定类型对象。

构造函数与不同函数的区别就是调用方式不同,任何函数只要使用new操作符调用就是构造函数,而不使用new操作调用的函数就是普通函数。

function Person(name,age)
{
    this.name = name;
    this.age = age;
    this.say = function(){
    	console.log(this.name,this.age);
    }
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // false

要创建Person的实例,应使用new操作符,以这种方式调用构造函数会执行如下操作:

  1. 在内存中创建一个新对象
  2. 在创建新对象内部设置__proto__隐式原型指向构造函数的prototype显示原型。
  3. 构造函数内部的this指向这个新创建的对象
  4. 执行构造函数内部的代码。
  5. 如果构造函数返回非空对象,则返回该对象,否则返回创建的新对象。

构造函数的问题

构造函数的主要问题在于:其定义的方法会在每个实例上都创建一遍,其实他们做的都是一样的事情,没有必要创建多次。那么就需要找到一个构造函数本身的全局属性来创建每个对象都能放到的属性,将方法定义到这个属性上,并且这个属性可以访问到对象的属性。这个属性就是原型(prototype)。

原型模式

每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。是用原型对象的好处是:在它上面定义的属性和方法可以被对象实例共享。

function Person(name,age)
{
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log(this.name);
}

let p1 = createPerson('atuotuo',20);
let p2 = createPerson('zs',21);
console.log(p1.say == p2.say) // true

继承

原型链继承

原型链继承的基本思想:通过原型继承多个引用类型的属性和方法。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score){
    this.score = score;
}
Student.prototype = new Person('atuotuo'); // 继承

let s1 = new Student(12);
s1.hobby.push('3');
let s2 = new Student(13);
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2,3

存在问题:

1. 原型中包含引用类型值的时候,原型中包含的引用值会在所有实例间共享。
1. 子类在实例化时不能给父类型的构造函数传参数。

构造函数继承

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
function Student(score,name){
    Person.call(this,name); // 继承
    this.score = score;
}

let s1 = new Student(12,'tuotuo');
s1.hobby.push('3');
let s2 = new Student(13'zs');
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:必须在构造函数中定义方法,原型上的方法没有继承

组合继承(原型链 + 构造函数)

组合继承综合了原型链和构造函数,将两者的有点集中起来。基本思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = new Person() //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

存在问题:父类被创建两次,而且在子的原型上还会出现父类的实例属性,会造成资源浪费。

原型式继承

原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合,但是原型式继承与原型链继承类似,属性中包含引用值始终会共享内存。

function object(o){
	function F(){};
	F.prototype = o;
	return new F();
}

寄生式继承

寄生式继承其实就是使用一个工厂函数对原型式继承进行封装,可以在新创建的对象添加其他属性和方法。

function object(o){
	function F(){};
	F.prototype = o;
	let obj = new F();
	obj.say = function(){
		console.log(1);
	}
	return obj;
}

寄生式组合继承

主要是解决组合继承的问题:父类构造函数始终会被调用两次。

基本思路:不通过调用父类的构造函数给子类原型赋值,而是取得父类原型的创建一个空对象然后赋值子类原型。就是使用寄生式继承来继承父类原型。

function Person(name){
    this.name = name;
    this.hobby = ['1','2']
}
Person.prototype.say = function(){
    console.log(this.name);
}
function Student(score,name){
    Person.call(this,name); // 继承实例属性
    this.score = score;
}
Student.prototype = Object.create(Person.prototype); //继承实例方法

let s1 = new Student(12,'atuotuo');
s1.hobby.push('3');
s1.say() // atuotuo
let s2 = new Student(13'zs');
s2.say() //zs
console.log(s1.hobby); //1,2,3
console.log(s2.hobby); //1,2

类(class)是ES6新的基础性语法糖结构,表面上看是可以支持的面向对象变量,但实际上背后使用的仍然是原型和构造函数的概念。

类定义

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

class Person{}

类构造函数

constructor关键字用于在类定义块内部创建类的构造函数

class Person
{
	constructor(name){
		this.name = name;
	}
}

实例、原型和类成员

class Person
{
	constructor(name){
        // 在构造函数内部,可以通过this定义实例成员
		this._name = name;
	}
    //在类块中定义的所有内容都会定义在类的原型上
    say(){
        console.log(this.name);
    }
    // 通过get 和 set设置获取和设置访问器
    get name(){
        return this._name;
    }
    
    set name(newName){
        this._name = newName;
    }
    // 静态方法,实际上就是定义在类上的方法
    static speak(){
        console.log('speak');
    }
}

继承

基础继承

使用extends关键字,可以单继承

class Person
{

}
class Student extends Person
{

}

调用父类构造函数和静态类

派生类的方法可以通过super关键字引用他们的原型。

class Person
{
	static speak(){
	
	}
}
class Student extends Person
{
	constructor(){
		super() // 在派生类的构造函数中使用super可以调用父类的构造函数
	}
    static speak(){
        super.speak() //在静态方法中可以通过super调用继承的类上的定义的静态方法
    }
}

注意点:

super只能在派生的构造函数和静态方法中使用

不能但是引用super关键字,要么它调用构造函数,要么引用静态方法

调用super() 会调用父类构造函数,并将返回的实例赋给this

在类构造函数中,不能再调用super()之前引用this。

抽象基类

抽象基类:可供其他类继承,但本身不会被实例化,es没有这种专门的语法,但是通过new.target很容易实现。new.target保证通过new关键字调用的类和函数。

// 抽象基类
class Person{
    constructor(){
        if(new.target === Person){
            throw new Error("该类为抽象类,不能直接实例化");
        }
    }
}

常见面试题

new操作符的实现原理

  1. 首先创建一个新的对象

  2. 设置原型,将对象的隐式原型设置为构造函数的 prototype 对象

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

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

    实现

    function objectFactory(){
        let newobject = null;
       	let constructor = Array.prototype.shift.call(arguments);
        
        let result = null;
        if(typeof constructor !== 'function'){
            return;
        }
        
        // 新建对象,让对象的原型为构造函数的原型对象
        newobject = Object.create(constructor.prototype);
        // 将新创建的对象作为,构造函数的this对象
        result = constructor.apply(newobject,arguments);
        
        // 判断返回值的结果
        let flag = result && (typeof result === "object" || typeof result == 'function');
        
        return flag ? result:newobject;
    }
    

对原型和原型链的理解

原型对象:每个构造函数内部都有一个prototype属性(原型对象),这个对象包含了该构造函数的所有实例共享的属性和方法,prototype.constructor 指向该对象。

隐式原型:当使用构造函数创建一个对象时,在这个对象内部将创建一个指针(__proto),指向原型对象。一般来说不可直接访问__proto,ES5新增一个方法可以访问,Object.getPrototypeOf(obj).

原型链:在js中万物皆对象,每个对象那个中都有指针__proto__,当访问一个对象中的属性时,如果对象内部不存在这个属性,则会再原型中寻找,如果原型中还没有,就去原型的原型中找,原型链的尽头是Object.prototype。

原型链图片.png

原型链的终点是什么

由于object是构造函数,原型中点是Object.prototype.__proto__,而 Object.prototype.__proto__ = null。所以原型链的重点为null。

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

使用 obj.hasOwnProperty() 方法来判断属性是否属于原型链上的属性

instanceof的原理及实现

instanceof是通过原型链来实现的,通过检测原型链中是否存在该类对象的原型相同

function instanceof(obj,cla)
{
    let proto = Object.getPrototypeOf(obj);

    while(proto)
    {
        if(proto == cla.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}