JavaScript 的核心是支持面向对象的,同时它也提供了强大灵活的 OOP 语言能力。面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性。
1.面向对象三要素
- 封装:一种把数据和相关的方法绑定在一起使用的方法。
- 继承:一个类可以继承另一个类的特征。如属性或方法。
- 多态:顾名思义「多种」「形态」。不同类可以定义相同的方法或属性。就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在两个类不是父子关系时(两个类是同一个父类的不同子类)成立。
2.面向对象术语
- Namespace 命名空间允许开发人员在一个独特,应用相关的名字的名称下捆绑所有功能的容器。 在JavaScript中,命名空间只是另一个包含方法,属性,对象的对象。Javascript中的普通对象和命名空间在语言层面上没有区别。
- Class 类定义对象的特征。它是对象的属性和方法的模板定义。
- Object 对象类的一个实例。
- Property 属性对象的特征,比如颜色。
- Method 方法对象的能力,比如行走。
- Constructor 构造函数对象初始化的瞬间,被调用的方法。通常它的名字与包含它的类一致。
3.什么是构造函数,和普通函数的区别
函数:用来封装并执行代码块。
构造函数:一种特殊的函数,与关键字new
一起使用,是用来生成实例对象的函数。作用是初始化对象, 即为对象赋初始值(添加属性和方法)。
区别:
- 构造函数是和
new
关键字⼀起使⽤的 - 函数用来执行代码块,而构造函数用来创建对象
- 普通函数一般遵循小驼峰命名规则,构造函数一般遵循大驼峰命名规则
- 调用后返回内容不同,普通函数直接返回return定义的内容,没有
return
默认为undefined
;而构造函数返回this
指向的对象
4.什么是原型,什么是原型链?
原型:在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会与之关联一个原型对象。函数天生自带一个prototype
属性(对象或构造函数实例自带__proto__
属性),这个属性指向函数的原型对象。每一个对象或函数都会从原型中“继承”属性。(只有函数有prototype
属性,而对象和数组本身没有。)
让我们用一张图表示构造函数和实例原型之间的关系:
原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。
原型链:对象之间的继承关系,在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
5.构造函数的原型和实例化对象之间的关系
函数天生自带一个prototype
属性,而这个属性指向一个对象(指向函数的原型对象),我们简称原型。
实例化对象会从构造函数的原型中继承其方法和属性,也就是继承prototype
对象中的属性和方法。
实例化的对象是怎么指向(怎么继承)的prototype
?
实例化对象是通过__proto__
属性继承的。这个属性指向构造函数的原型对象prototype
。换句话说也就是__proto__
可以访问prototype
里面的所有属性和方法,__proto__
与prototype
是同一个东西,其内存地址相同,__proto__ === prototype // true
。
prototype
作用:
- 节约内存
- 扩展属性和方法
- 可以实现类之间的继承
prototype
、__proto__
、实例化对象三者之间的关系:
- 每一个对象都有一个__proto__属性
__proto__
指向创建自己的那个构造函数的原型- 对象可以直接访问自己
__proto__
里面的属性和方法 - constructor 指向创建自己的那个构造函数
({}).constructor === Object; // true
Object.constructor === Function; // true
6.new关键字做了哪些事情(实例化对象或构造函数执行做了哪些事情)
new操作新建了一个对象(实例化构造函数)。这个对象原型链__proto__
指向构造函数的prototype
,执行构造函数后返回这个对象。构造函数被传入参数被调用,关键字this被指向该实例obj。返回实例obj。
- 在内存中开辟了一块空间
- 新生成了一个对象
- 链接到当前构造函数的原型
- 绑定 this,指向当前实例化的对象
- 返回这个新对象
隐式操作
开辟新的堆内存,构造函数的执行体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上定义方法的区别
函数内的方法相当于是函数的私有变量,如果新创建的对象需要使用里面的私有变量,就使用函数内的方法。私有变量实现了面向对象的多态性。
在原型链上定义方法一般用于让所有实例和子类继承此方法。体现了面向对象的继承性。
区别:
- 定义在构造函数内部的方法,会在它的每一个实例上都克隆(重新定义)这个方法,消耗的内存更高。
- 定义在
prototype
属性上的方法会让它的所有实例和子类都共享这个方法,不会重新复制。原型链上的方法指向一个内存引索,所有实例都是直接引用,更节省内存。 - 使用
prototype
的方法在继承的时候是共有的,多个实例或继承对象共有这个方法,所以一旦改变,所有的都改变。 - 在构造函数中定义的属性和方法要比原型
prototype
中定义的属性和方法优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。 - 定义在构造函数中的属性会被序列化,而
prototype
中的属性不会被序列化。所以使用工厂模式定义对象的时候不能用原型prototype
定义属性。
9.改变函数内部this指针的方法有哪些,区别是什么?
call,apply,bind三个方法。他们的第一个参数都是要改变的this指向。第二个参数是传给当前函数的参数。
这三个方法的作用是:①改变this指向;②借用别的对象的方法;③调用函数立即执行如call和apply。他们的区别如下:
- call:第二个参数是普通函数参数类型,可以有多个参数,使用call方法会立即执行当前函数。如
Array.slice.call(this,arg1,arg2,arg3)
- apply:第二个参数是一个数组类型。apply只有两个参数。使用apply方法会立即执行当前函数。
- bind:第二个参数与call一致。bind方法不会立即执行当前函数,而是返回一个新的函数,需要再次调用(柯里化函数)。
- 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)
}
① 原型链继承
方法:子类的原型指向父类的一个实例。这就是基于原型链的继承。
优点:
- 基于原型链,既是父类的实例,也是子类的实例。
- 可以继承父类的属性和构造方法,也能继承父类原型属性和方法。
缺点:无法实现多继承。
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
② 构造继承
方法:调用父类的构造函数来实现继承。相当于复制父类的构造属性和方法给子类。
优点:
- 可以实现多继承(继承多个父类的构造方法)
- 实例只是子类的实例,不是父类的实例
- 子类保留父类构造函数可传参的优势,如下例子传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
③ 组合继承
方法:即使用原型链继承和构造继承组合的方式实现继承。通过调用父类构造,继承父类的属性并保留传参的优点。然后通过将父类实例作为子类的原型,实现继承。
优点:
- 可以继承实例属性方法,也可以继承原型属性和方法。
- 可以实现多继承。
缺点:调用了两次父类函数,影响了性能,增加了内存负担。(在构造函数中调用了一次父类构造方法,在原型链继承时又初始化了一次父类)
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
⑤ 寄生组合继承-推荐
方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。
优点:
- 避免初始化父类两次造成内存浪费。
- 既能继承父类的原型属性和方法,也能继承父类的构造属性和方法。
- 可实现多继承。
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指向问题
- 默认绑定:全局环境中,this默认绑定到window。独立函数调用(直接调用,不使用a.b())都指向window。
- 隐式绑定:在对象字面量中,this隐式绑定到该对象。如:
var obj = {a:1, fn:function(){ console.log(this.a) } }
- 隐式丢失:被隐式绑定的函数丢失绑定对象,从而this被绑到window。
- 显式绑定:使用call/apply/bind方法把对象绑定到this上
- new绑定:用new关键字实例化构造函数,this被绑到到实例化对象上。
- 函数中的this指向当前调用函数所处的环境,谁调用了它,this就指向谁。
- this指向和书写位置无关,和调用方式有关(this是在函数调用时入栈的执行上下文中动态绑定的)
- 优先级: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,转载须征得作者授权。