Menu

js知识总结–面向对象篇

JavaScript 的核心是支持面向对象的,同时它也提供了强大灵活的 OOP 语言能力。面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性。

1.面向对象三要素

  1. 封装:一种把数据和相关的方法绑定在一起使用的方法。
  2. 继承:一个类可以继承另一个类的特征。如属性或方法。
  3. 多态:顾名思义「多种」「形态」。不同类可以定义相同的方法或属性。就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在两个类不是父子关系时(两个类是同一个父类的不同子类)成立。

2.面向对象术语

  1. Namespace 命名空间允许开发人员在一个独特,应用相关的名字的名称下捆绑所有功能的容器。 在JavaScript中,命名空间只是另一个包含方法,属性,对象的对象。Javascript中的普通对象和命名空间在语言层面上没有区别。
  2. Class 类定义对象的特征。它是对象的属性和方法的模板定义。
  3. Object 对象类的一个实例。
  4. Property 属性对象的特征,比如颜色。
  5. Method 方法对象的能力,比如行走。
  6. Constructor 构造函数对象初始化的瞬间,被调用的方法。通常它的名字与包含它的类一致。

3.什么是构造函数,和普通函数的区别

函数:用来封装并执行代码块。

构造函数:一种特殊的函数,与关键字new一起使用,是用来生成实例对象的函数。作用是初始化对象, 即为对象赋初始值(添加属性和方法)。

区别:

  1. 构造函数是和new 关键字⼀起使⽤的
  2. 函数用来执行代码块,而构造函数用来创建对象
  3. 普通函数一般遵循小驼峰命名规则,构造函数一般遵循大驼峰命名规则
  4. 调用后返回内容不同,普通函数直接返回return定义的内容,没有return默认为undefined;而构造函数返回this指向的对象

4.什么是原型,什么是原型链?

原型:在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会与之关联一个原型对象。函数天生自带一个prototype属性(对象或构造函数实例自带__proto__属性),这个属性指向函数的原型对象。每一个对象或函数都会从原型中“继承”属性。(只有函数有prototype属性,而对象和数组本身没有。)

让我们用一张图表示构造函数和实例原型之间的关系:

js知识总结--面向对象篇

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。

原型链:对象之间的继承关系,在JS中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条。由相互关联的原型组成的链状结构就是原型链

举例说明:person → Person → Object ,普通人继承人类,人类继承对象类

当我们访问对象的一个属性或方法时,它会先在对象自身(私有属性或方法)中寻找(判断是否为私有属性或方法而非继承来的,使用hasOwnProperty(key)方法),如果有则直接使用,如果没有则会去原型对象中寻找(构造函数的构造属性和方法优先级大于原型上属性和方法),如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则会找到null,返回undefined。null是原型链顶层。

我们可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true

function Person() {}
Person.prototype.a = 123;
Person.prototype.sayHello = function () {
    alert("hello");
};
var person = new Person()
console.log(person.a) //123
console.log(person.hasOwnProperty('a')) //false
console.log('a'in person) //true
console.log(person.__proto__===Person.prototype)   //true

person实例中没有a这个属性,从 person 对象中找不到 a 属性就会从 person 的原型也就是 person.__proto__,也就是 Person.prototype中查找,很幸运地得到a的值为123。那假如 person.__proto__中也没有该属性,又该如何查找?

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层Object为止。Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有__proto__这个属性。

console.log(Object.prototype.__proto__ === null) // true

js知识总结--面向对象篇

5.构造函数的原型和实例化对象之间的关系

函数天生自带一个prototype属性,而这个属性指向一个对象(指向函数的原型对象),我们简称原型。

实例化对象会从构造函数的原型中继承其方法和属性,也就是继承prototype对象中的属性和方法。

实例化的对象是怎么指向(怎么继承)的prototype

实例化对象是通过__proto__属性继承的。这个属性指向构造函数的原型对象prototype。换句话说也就是__proto__可以访问prototype里面的所有属性和方法,__proto__prototype是同一个东西,其内存地址相同,__proto__ === prototype // true

prototype作用:

  1. 节约内存
  2. 扩展属性和方法
  3. 可以实现类之间的继承

prototype__proto__、实例化对象三者之间的关系:

  1. 每一个对象都有一个__proto__属性
  2. __proto__指向创建自己的那个构造函数的原型
  3. 对象可以直接访问自己__proto__里面的属性和方法
  4. constructor 指向创建自己的那个构造函数
({}).constructor === Object; // true  
Object.constructor === Function; // true

6.new关键字做了哪些事情(实例化对象或构造函数执行做了哪些事情)

new操作新建了一个对象(实例化构造函数)。这个对象原型链__proto__指向构造函数的prototype,执行构造函数后返回这个对象。构造函数被传入参数被调用,关键字this被指向该实例obj。返回实例obj。

  1. 在内存中开辟了一块空间
  2. 新生成了一个对象
  3. 链接到当前构造函数的原型
  4. 绑定 this,指向当前实例化的对象
  5. 返回这个新对象

隐式操作

开辟新的堆内存,构造函数的执行体this会指向这个堆。代码执行完毕后,会把这个堆内存地址返回并在栈中赋给新的变量。

构造函数执行前,会在函数体最前面添加一个 this = {},然后按照顺序执行代码,然后最后会有 return this; 其中 添加 this={} 以及 return this; 操作是隐式操作,js引擎会自动帮我们执行。

假如强行要改变构造函数返回的结果也是可以的,可以在构造函数里面添加return语句,但是除了{}[]、以及function类型会被如实返回,其他return的情况都会默认返回原来的构造函数的对象。

用函数方法手写一个new

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

function aaa(a){
    this.a = a
}

new aaa(1) //aaa {a: 1}
create(aaa,1) //aaa {a:1}

function bbb(a){
    return {a:a}
}

new bbb(1) //{a: 1}
create(bbb,1) //{a:1}

7.原生js模拟ES6的类CLASS

//创建类-这个函数也相当于class中的constructor构造函数。
function Person(firstName) {
  this.firstName = firstName; //类的属性,可以用this关键字定义
  alert('Person instantiated');
}

//类的方法(原型链上创建)
Person.prototype.sayHello = function() {
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器,以便在实例化Student时能够执行Person的构造函数
  // 以便继承父类Person的属性
  Person.call(this, firstName);

  // 初始化Student类私有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
// 用于继承Person的原型链方法
// 这里最好不要直接Student.prototype = Person.prototype,
// 因为prototype是引用类型原因,一旦子类更改相应的方法,父类及其他子类都会继承。
Student.prototype = Object.create(Person.prototype); 
//Student.prototype = new Person(); //或者这样写

// 在不支持Object.create方法的浏览器中可以使用如下方法进行继承
// 这种方式叫做寄生类(中间类)方式,可以砍掉父类的构造函数,避免多次调用。
// 也可以避免直接使用Student.prototype  = Person.prototype造成的单一引用
function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}
// Usage:
Student.prototype = createObject(Person.prototype);

// 设置"constructor" 属性指向Student
// js 并不检测子类的 prototype.constructor,我们必须手动申明
// 若不指定,则constructor指向Person
Student.prototype.constructor = Student;

// 更换"sayHello" 方法,这里不会影响父类的sayHello方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

//实例化类,实例化时候构造函数会被立即执行
var person1 = new Person('Alice');//执行alert
var person2 = new Person('Bob'); //执行alert

//类的属性可以在实例化对象后访问
alert('person1 is ' + person1.firstName); // alerts "person1 is Alice"
alert('person2 is ' + person2.firstName); // alerts "person2 is Bob"

//调用类的方法
person1.sayHello(); // alerts "Hello, I'm Alice"
person2.sayHello(); // alerts "Hello, I'm Bob"

封装的体现:Student类虽然不需要知道Person类的walk()方法是如何实现的,但是仍然可以使用这个方法;Student类不需要明确地定义这个方法,除非我们想改变它。 这就叫做封装。

继承的体现:Student类作为 Person类的子类.可以继承父类的所有属性和方法。

多态的体现:我们重定义了sayHello() 方法并添加了 sayGoodBye() 方法.

8.构造函数里的方法和prototype上定义方法的区别

函数内的方法相当于是函数的私有变量,如果新创建的对象需要使用里面的私有变量,就使用函数内的方法。私有变量实现了面向对象的多态性。

在原型链上定义方法一般用于让所有实例和子类继承此方法。体现了面向对象的继承性。

区别:

  1. 定义在构造函数内部的方法,会在它的每一个实例上都克隆(重新定义)这个方法,消耗的内存更高。
  2. 定义在prototype 属性上的方法会让它的所有实例和子类都共享这个方法,不会重新复制。原型链上的方法指向一个内存引索,所有实例都是直接引用,更节省内存。
  3. 使用prototype的方法在继承的时候是共有的,多个实例或继承对象共有这个方法,所以一旦改变,所有的都改变。
  4. 在构造函数中定义的属性和方法要比原型prototype中定义的属性和方法优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。
  5. 定义在构造函数中的属性会被序列化,而prototype中的属性不会被序列化。所以使用工厂模式定义对象的时候不能用原型prototype定义属性。

9.改变函数内部this指针的方法有哪些,区别是什么?

call,apply,bind三个方法。他们的第一个参数都是要改变的this指向。第二个参数是传给当前函数的参数。

这三个方法的作用是:①改变this指向;②借用别的对象的方法;③调用函数立即执行如call和apply。他们的区别如下:

  1. call:第二个参数是普通函数参数类型,可以有多个参数,使用call方法会立即执行当前函数。如Array.slice.call(this,arg1,arg2,arg3)
  2. apply:第二个参数是一个数组类型。apply只有两个参数。使用apply方法会立即执行当前函数。
  3. bind:第二个参数与call一致。bind方法不会立即执行当前函数,而是返回一个新的函数,需要再次调用(柯里化函数)。
  4. call()性能更佳。

手写call

Function.prototype.myCall = function(context){
	// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
	// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
	context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

	// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
	// 函数中的this指向调用它的对象
	context.fn = this
	const res = context.fn(...[...arguments].slice(1))

	//删除这个挂载的函数
	delete context.fn

	// 将执行结果返回
	return res
}

10.单继承和多继承

单继承:一个子类只有一个父类

多继承:一个子类可以有多个父类

11.构造函数的多种继承方式及其优缺点

先定义一个Animal类:

function Animal(name){
	// 属性
	this.name = name || "Animal"
	
	//实例方法
	this.sleep = function(){
		console.log(this.name + "在睡觉")
	}
}

// 原型方法
Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

① 原型链继承

方法:子类的原型指向父类的一个实例。这就是基于原型链的继承。

优点:

  1. 基于原型链,既是父类的实例,也是子类的实例。
  2. 可以继承父类的属性和构造方法,也能继承父类原型属性和方法。

缺点:无法实现多继承。

Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

function Cat(){
	this.age = 10
}
Cat.prototype = new Animal()
Cat.prototype.name = "tom"
//以上两行也可以改为Cat.prototype = new Animal("tom")

// 测试
var cat = new Cat()
console.log(cat.name)	//tom
console.log(cat.age) //10
cat.eat("fish") //tom在吃fish
cat.sleep() //tom在睡觉
console.log(cat instanceof Animal) //true
console.log(cat instanceof Cat) //true
console.log(cat.constructor == Animal) //true 若不指定,则指向父类
console.log(cat.constructor == Cat) //false

// 指定构造函数
Cat.prototype.constructor = Cat
console.log(cat.constructor == Cat) //true

② 构造继承

方法:调用父类的构造函数来实现继承。相当于复制父类的构造属性和方法给子类。

优点:

  1. 可以实现多继承(继承多个父类的构造方法)
  2. 实例只是子类的实例,不是父类的实例
  3. 子类保留父类构造函数可传参的优势,如下例子传name

缺点:只能继承父类的构造属性和方法,无法继承父类的原型属性和方法。

function Dog(name){
	Animal.call(this,name) //调用父类构造方法,将this传与父类
        //这里可以调用多个父类,来实现多继承
}

// 测试
var dog = new Dog("wangwang")
console.log(dog.name)	//wangwang
dog.eat("rou")	//Uncaught TypeError: dog.eat is not a function
dog.sleep() //wangwang在睡觉
console.log(dog instanceof Animal) //false
console.log(dog instanceof Dog) //true
console.log(dog.constructor == Animal) //false
console.log(dog.constructor == Dog) //true

③ 组合继承

方法:即使用原型链继承和构造继承组合的方式实现继承。通过调用父类构造,继承父类的属性并保留传参的优点。然后通过将父类实例作为子类的原型,实现继承。

优点:

  1. 可以继承实例属性方法,也可以继承原型属性和方法。
  2. 可以实现多继承。

缺点:调用了两次父类函数,影响了性能,增加了内存负担。(在构造函数中调用了一次父类构造方法,在原型链继承时又初始化了一次父类)

function Cat(name){
	Animal.call(this,name);
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 测试
var cat = new Cat("tom")
console.log(cat.name)	//tom
cat.sleep()	//tom在睡觉
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
console.log(pig.constructor == Animal) //false 由于重新将Cat赋值给了constructor
console.log(pig.constructor == Cat) //true

④ 寄生类继承

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:由于原型链prototype是引用类型,无法直接将其赋值给子类的原型,会被子类直接修改。所以赋值给中间类,然后将中间类的实例赋给子类原型,避免了原型引用造成的原型开放(易被子类修改)。且直接赋值原型给寄生类,避免初始化父类而再次执行父类造成内存浪费。

缺点:只能继承父类的原型属性和方法,无法继承父类的构造属性和方法。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//创建寄生类实例
function superObj(proto) {
    function Super() {}
    Super.prototype = proto
    return new Super()
}

//将寄生类实例作为子类的原型
Pig.prototype = superObj(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

以上寄生类方法可以使用Object.create()方法取代。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//将寄生类实例作为子类的原型
//Object.create()方法会创建一个新对象,类似于实例化的类
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

⑤ 寄生组合继承-推荐

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:

  1. 避免初始化父类两次造成内存浪费。
  2. 既能继承父类的原型属性和方法,也能继承父类的构造属性和方法。
  3. 可实现多继承。
function Pig(name){
	Animal.call(this,name); //调用父类,将其属性和方法拷贝给子类
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

可在构造函数中调用多个父类,实现构造属性和方法的多继承。原型属性和方法多继承可以使用Object.assign(cur.prototype,otherFather.prototype)方法实现。

function Pig(name){
	Animal.call(this,name);
	Object1.call(this)
	Object2.call(this)
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//Object.assign 会把其他原型上的属性和方法拷贝到Pig原型上
//只是原型方法的拷贝,而实例原型还是Animal
//若不支持Object.assign,则使用for.in遍历将对象合并
Object.assign(Pig.prototype,
	Object1.prototype,
	Object2.prototype,
	{a:1,b:2},
)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig instanceof Object1) //false
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false
console.log(pig.constructor == Object1) //false

12.this指向问题

  1. 默认绑定:全局环境中,this默认绑定到window。独立函数调用(直接调用,不使用a.b())都指向window。
  2. 隐式绑定:在对象字面量中,this隐式绑定到该对象。如:var obj = {a:1, fn:function(){ console.log(this.a) } }
  3. 隐式丢失:被隐式绑定的函数丢失绑定对象,从而this被绑到window。
  4. 显式绑定:使用call/apply/bind方法把对象绑定到this上
  5. new绑定:用new关键字实例化构造函数,this被绑到到实例化对象上。
  6. 函数中的this指向当前调用函数所处的环境,谁调用了它,this就指向谁。
  7. this指向和书写位置无关,和调用方式有关(this是在函数调用时入栈的执行上下文中动态绑定的)
  8. 优先级:new > 显式绑定(bind) > 隐式绑定 > 默认绑定

独立函数调用:

fn1(){
  console.log(this)  // window
}
fn2(){
  console.log(this)  // window
  fn1()
}
fn2()

// 以上都是window
var a = 1
function foo() {
	console.log(this.a)
}
foo()  //1
//函数中的this指向取决于谁调用了它
//foo() 相当于 window.foo(),所以指向window
//而var声明的变量会被挂载到window上

var obj = {
	a: 2,
	foo: foo
}
obj.foo() //2
//这里是obj调用了foo()所以this指向obj

// 以上两者情况 this只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,this`只会绑定在 c上,不会被任何方式修改 this指向
var c = new foo()
c.a = 3
console.log(c.a)  //3

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

构造函数默认不使用return关键字,他们通常初始化新对象。当构造函数函数体执行完毕后会显式返回这个对象。

箭头函数this指向于定义时所处的外层非箭头函数(注意这里是函数,若为对象则无效,再找外层)环境(取决于他外面的第一个不是箭头函数的函数this),一旦声明(绑定了上下文)无法改变。

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
a()()()  //window
//等同于window.a()()()
//这里的this指向函数a的this,而函数a的this在调用时是window调用的,所以指向window

//--------------------------------

var obj = {
	fn:() => {
		console.log(this)
	},
	fn2:function() {
		console.log(this)
	}
}
obj.fn() //window 由于箭头函数定义时外层是对象不是函数,那么再找外层,所以是window
obj.fn2() //obj

13.instanceof原理

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。若找不到,会一直判断父类的prototype,直至Object

手写一个 instanceof

function _instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__  //这里如果找不到则找其原型的原型。比如数组最终原型指向Object
    }
}

_instanceof([],Array) //true
_instanceof([],Object) //true
[] instanceof Array //true
[] instanceof Object //true

本文固定连接:https://code.zuifengyun.com/2018/07/1650.html,转载须征得作者授权。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持