面向过程和面向对象
面向过程
就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
经典案例 :
(1) 购物车 (1. 获取元素 2. 加号事件 3.减号事件 4.全选/反选 5.单删和全删 6.合计)
(2) 元素拖拽(1. 获取元素 2. 当鼠标按下,确定按下的位置 3.鼠标在文档移动时,盒子跟随移动 4.鼠标抬起,停止移动)
面向对象
思想上提升 , 就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与行为(方法)–这个对象就称之为类
也就是说 把构成问题的事务(事务:一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit) ),分解成各个对象,建立对象的目的是为了描述某个事物在整个解决问题的步骤中的行为
举例说明
把大象放进冰箱中需要几步
(1) 面向过程 (分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现)
1.1 需要的事物 大象 冰箱
1.2 打开冰箱
1.3 把大象放进冰箱中
1.4 关上冰箱
(2) 面向对象 (把大象放进冰箱)
冰箱的对象 = {
target:"大象", // 猪肉,苹果,脆脆冰
打开:方法
放东西:方法 // 放target
关闭:方法
}
面向对象的封装
简易案例
需求 描述一只猫的名称,颜色,叫声,技能
- 面向过程 分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现
面向过程
var name = "Tom";
var color = "black";
function say() {
console.log("喵喵");
}
function skill() {
console.log("抓老鼠");
}
- 将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与行为(方法)–这个对象就称之为类
字面量创建对象 (单个)
var cat = {
name: "Tom",
color: "black",
say: function () {
console.log(this.name, this.color)
},
skill: function () {
console.log("抓老鼠");
}
}
但是以字面量创建的对象存在一个明显的缺点,每次声明只能创建一个,无法实现代码的复用
var cat = {
name: "Kitty",
color: "pink",
say: function () {
console.log(this.name, this.color)
},
skill: function () {
console.log("抓老鼠");
}
}
解决方式 工厂模式 => 封装函数(提取参数 返回对象)
工厂模式 批量创建对象
function cat(name, color) {
var obj = {
name: name,
color: color,
say: function () {
console.log(this.name, this.color)
},
skill: function () {
console.log("抓老鼠");
}
}
return obj;
}
改写形式
function cat(name, color) {
var obj = new Object();
obj.name = name;
obj.color = color;
obj.say = function () {
console.log(this.name, this.color)
}
obj.skill = function () {
console.log("抓老鼠");
}
return obj;
}
var tom = cat("Tom", "black");
console.log(tom);
var Kitty = cat("Kitty", "pink");
console.log(Kitty)
工厂模式 批量创建对象
-
优点 快速得到想要的对象
-
缺点
(1) 通过工厂模式创建的对象跟函数没有任何关联
对比Array 非常明显
通过new 构造函数创建的对象,和函数本身有联系
var arr = new Array(); //构造函数
console.log(arr instanceof Array); // true
(2)公用的属性和方法重复声明 需要占据多余内存
{say,skill} 两个方法重复声明 4次 占内存
解决方式 构造函数
// 构造函数首字母大写
function Cat(name, color) {
//
this.name = name;
this.color = color;
}
构造函数创建了一个原型对象(prototype) 所有的公用属性和方法 都存储在原型对象上, 每一个通过构造函数创建的对象都可以使用该原型对象的方法
Cat.prototype.species = "猫";
Cat.prototype.skill = function () {
console.log("捉老鼠");
}
Cat.prototype.say = function () {
console.log(this.name, this.color);
}
函数调用 通过构造函数new 创建实例化对象
var Tom = new Cat("Tom", "black");
console.log(Tom);
console.log(Tom.name);
console.log(Tom.color);
Tom.say(); // 函数在调用的时候属于谁 就指向谁 this -> Tom
1. 通过构造函数创建的对象和 构造函数的关联性 Tom instanceof Cat => true;
2. 共有的属性和方法重复声明的问题 => 怎么解决
构造函数创建了一个原型对象(prototype) 所有的公用属性和方法 都存储在原型对象上, 每一个通过构造函数创建的对象都可以使用该原型对象的方法
注意:
prototype是一个指针,指向一个对象。这个对象的用途是,包含所有实例共享的属性和方法。
所有通过同一个构造函数创建的实例对象,都会共享同一个 prototype。
原型诞生的意义就是可以实现代码复用。
JS的new关键词做了什么操作?
构造函数首字母大写
new操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象
1、在函数内部创建一个空的对象(默认操作,看不到)
2、链接到原型
3、绑定this指向,执行构造函数
4、确保返回的是对象
以刚刚的方法为例,new共经过了4个阶段
1、创建一个空对象
var obj = new Object();
2、设置原型链(当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象)
obj.__proto__= Cat.prototype;
3、让Cat中的this指向obj,并执行Cat的函数体。(创建新的对象之后,将构造函数的作用域赋给新对象(因此this就指向了这个新对象)),函数调用时属于谁,this就指向谁
var result = Cat.call(obj);
4、判断构造函数的返回值类型:
- 如果是值类型(number,string,boolean,null,undefined),构造函数默认返回,第一步创建的对象obj。
- 如果是引用类型,就返回这个引用类型的对象
默认情况下函数返回值为undefined,即没有显示定义返回值的话,但构造函数例外,new构造函数在没有return的情况下默认返回新创建的对象。
原型对象prototype(显示原型)
- 构造函数的动态方法=>原型对象{}(一个对象)
- 目的:存放实例化对象的公有属性和方法(所有经由此构造函数创建的实例化对象,均可调用)
- 函数调用时属于谁,this就指向谁
原型对象的constructor 构造器
作用
- 指向构造函数本身
- 是由实例化对象来调用(帮助识别 实例化对象是由哪个构造函数创建的)
- 所有实例化对象的原型 都指向 构造函数的原型对象
原型属性_proto_
(隐式原型)
原型属性(隐式原型 书写的时候省略)
-
所有实例化对象的原型属性(
_proto_
)都指向,构造函数的原型对象(prototype) -
调用方法和属性的时候,先看自己有没有
- 有,找自己的(Tom.color)
- 没有,找原型对象(
_proto_
=>Cat.prototype )
原型和原型链
每个继承父函数的子函数的对象都包含一个内部属性_proto_。该属性包含一个指针,指向父函数的prototype。若父函数的原型对象的_proto_属性为再上一层函数。在此过程中就形成了原型链
注: 平时我们查找一个是实例化对象的属性和方法时 就是顺着原型链查找的.
最终找不到,返回underfined
改写为构造函数
注意:
-
此种写法解决了对象和函数的关系性问题
-
公有的属性和方法还是重复声明
-
把共有的属性和方法提到一个对象上(原型对象prototype用存储共有的属性和方法)
-
所有的实例化对象都可以使用原型对象(prototype)上共有的属性和方法
对比Array
数组的实例化方法
push pop shift unshift
Array的静态方法 自己调用
- Array.from()
- Array.isArray()
Array 动态方法(原型对象)
构造函数的静态属性和方法
(构造函数自己调用)
动态的属性和方法
实例化对象查找属性和方法时,遵从就近原则
Object原型对象的属性
- 所有的实例化对象 => 原型链 => Object.prototype
- 所有的实例化对象都可以使用Object.prototype上的方法
hasOwnProperty 是否拥有属性
isPrototypeOf
原型链的相关方法
instanceof
判断某个实例化对象的原型链上是否存在某个构造函数的对象
isPrototypeOf()
判断某个构造函数的原型对象是否存在于 某个实例化对象的原型链上
in
判断的一个对象(实例对象)的原型链上是否存在某个属性
hasOwnProperty()
判断在实例中是否包含该属性,不在原型链中查找该属性
Function都是由Function创建的
拓展方法
instanceof 判断的一个对象(实例对象)的原型链上是否包含构造函数的原型对象(prototype)
Tom instanceof Cat;
isPrototypeOf 判断的是构造函数的原型对象是否存在于实例对象的原型链之上
Cat.prototype.isPrototypeOf(Tom)
in 判断的一个对象(实例对象)的原型链上是否存在某个属性
"name" in Cat
hasOwnProperty 在实例中是否包含该属性,不在原型中查找该属性
Tom.hasOwnProperty('name')
propertyIsEnumerable 方法返回一个布尔值,表示指定的属性是否可枚举(遍历)。
Tom.propertyIsEnumerable("name")
ES6 构造函数(类)
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。
function Cat(name, color) {
this.name = name;
this.color = color;
}
构造函数创建了一个原型对象(prototype) 所有的公用属性和方法 都存储在原型对象上, 每一个通过构造函数创建的对象都可以使用该原型对象的方法
Cat.prototype.species = "猫";
Cat.prototype.skill = function () {
console.log("捉老鼠");
}
Cat.prototype.say = function () {
console.log(this.name, this.color);
}
函数调用 通过构造函数new 创建实例化对象
var Tom = new Cat("Tom", "black");
console.log(Tom);
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已.
-
静态属性和方法(关键词 static 定义),只能自己实例对象使用
-
默认都是动态的
-
手动将属性绑定到原型对象上
-
// ES6
class Cat{
constructor(name,color) {
this.name = name;
this.color = color;
}
// 静态属性和方法(关键词 static定义)
static msg = "hello this is Cart";
static isCat(arg){
if(arg.constructor === Cat){// 数组由构造函数创建
return false;
}
}
species = "猫";
// 动态方法
call(){
console.log("喵喵");
}
skill() {
console.log("捉老鼠");
}
say(){
console.log(this,this.name,this.color)
}
}
Cat.prototype.species ="猫species"; //手动将属性绑定到原型对象上
console.dir(Cat);
ES6 此种写法类似于
Cat.prototype={
constructor(){},
call(){},
skill(){},
say(){}
}
call apply bind
关于call和apply
如何改变的this的指向
call apply bind(所有的函数都可以使用call apply bind这三个方法)
call
call(obj,arg1,arg2,arg3,…argN)
- 立即调用函数(不改变函数本身)
- 调用函数,在函数解释执行时,把this强制指向call() 方法中的第一个参数(对象)
- 如果函数需要接收参数,则从第二个参数开始,依次往后传
apply
apply(obj,[arg1,arg2,arg3…argN])
- 立即调用函数(不改变函数本身)
- 调用函数,在函数解释执行时,把this强制指向apply() 方法中的第一个参数(对象)
- 如果函数需要接收参数,apply 则需要先将参数依次放到数组中,再将数组作为第二参数,传入到apply中
bind()
bind(obj,arg1,arg2,arg3,…argN)
类似call方法
- 不会立即执行函数,而是返回一个新的函数
- 调用新的函数,在新的函数解释执行时,可以把this强制指向bind()方法中的第一个参数(对象)
- 如果函数需要接收参数,则从第二个参数开始,依次往后传
拆开写
面向对象的继承
让 一个类继承(构造函数) 另一个类(构造函数) 的 属性和方法(实例化对象的属性和方法 和 原型对象prototype上的属性和方法) 或者重新定义、追加属性和方法等。
继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
继承的类 叫做 子类 , 被继承的类 叫做 父类 。
继承主要解决的是代码重复使用的问题。
// 人
function Person(name, age) {
this.name = name;
this.age = age;
this.species = "human";
}
Person.prototype.skill = function () {
consoel.log("think");
}
Person.prototype.say = function () {
consoel.log(this.name, this.age);
}
// 黄种人
function YellowPerson(hobby) {
this.hobby = hobby;
}
YellowPerson.prototype.skin = "yellow";
YellowPerson.prototype.speak = function () {
console.log("chinese");
};
上述代码有两个类 Person 和 YellowPerson , 如何让YellowPerson 继承 Person的name,age,species属性和 skill,say方法?
构造函数继承
// 人
function Person(name, age) { // 构造函数中 实例的属性和方法
// this->构造函数Person创建的实例化对象
this.name = name;
this.age = age;
this.species = "human";
// this.__proto__ = Person.prototype
// return this;
}
// 原型对象上的属性和方法
Person.prototype.say = function () {
console.log(this.name, this.age);
}
Person.prototype.skill = function () {
console.log("thinking");
}
// YellowPerson
function YellowPerson(name, age, hobby) {
// this->构造函数YellowPerson创建的实例化对象
// Person(); // 普通函数来调用 this -> window
// 我们的想法是 调用Person()函数 将Person()函数中的this指向 当前YellowPerson创建的实例化对象(this)
// 1. 执行Person()方法
// 2. 在Person()执行过程中 将this 只想为 当前YellowPerson创建的实例化对象(this)
Person.call(this, name, age);
this.hobby = hobby;
}
var zhangSan = new YellowPerson("张三", 18, "唱");
console.log(zhangSan)
缺点
- 只能继承 父类实例的属性(name,age,species)和方法
- 并不能继承 父类原型对象(prototype)上的属性和方法
call和apply都会调 用函数 , 在函数执行过程中改变this的指向
call(obj,arg1,arg2...argN)
1. 执行函数
2. 在函数执行过程中,将该函数的this 强制指向call的第一个参数(obj)
3. 如果函数需要接收参数的话 那么在call()的第二个参数起依次向后排
function fn(a, b) {
console.log(this, a, b); // document 10,20
}
fn.call(document, 10, 20);
apply(obj,[arg1,arg2,...,argN])
1. 执行函数
2. 在函数执行过程中, 将该函数的this 强制指向apply的第一个参数(对象)
3. 如果函数需要接收参数的话 那么在apply() 需要将参数先依次放到数组中,在将数组作为第二参数传到apply()中
function fn(a, b) {
console.log(this, a, b);
}
fn.apply(document.body, [10, 20]);
call apply bind拓展用法
运用 1. 伪数组转真数组
var liList = document.querySelectorAll(".list li");
list = Array.prototype.slice(lilist);
console.log(list);
运用2. 判断一个数据是否是对象
var obj = {};
Object.prototype.toString.call(obj); // "[object Object]"
// 第一个小写的object表示类型
// 第二个大写的Object表示由谁创建的
原型继承
拓宽原型链
// 人
function Person(name, age) { // 构造函数中 实例的属性和方法
// var obj = new Object(); this->构造函数Person创建的实例化对象
this.name = name;
this.age = age;
this.species = "human";
// this.__proto__ = Person.prototype
// return this;
}
// 原型对象上的属性和方法
Person.prototype.say = function () {
console.log(this.name, this.age);
}
Person.prototype.skill = function () {
console.log("thinking");
}
// YellowPerson
function YellowPerson(name, age, hobby) {
this.hobby = hobby;
}
YellowPerson.prototype = new Person("张三", 18);
YellowPerson.prototype.sayHello = function () {
console.log("hello");
}
var zhangSan = new YellowPerson("张三", 18, "唱");
console.log(zhangSan)
// 拆分
// 1. new Person("张三", 18);
// 得到一个对象
// {
// name: "张三",
// age: 18,
// hobby: "human",
// __proto__: Person.prototype
// }
// 2. YellowPerson.prototype = new Person("张三", 18);
// YellowPerson.prototype = {
// name: "张三",
// age: 18,
// species: "human",
// __proto__: Person.prototype
// }
原型链
拓宽原型链(用Person的实例化对象覆盖YellowPerson的原型对象)
原型继承 通过拓宽原型链实现子类继承父类的原型对象的属性和方法
缺点
- 只能继承 父类原型对象上的属性和方法
- 并不能继承 父类实例的属性和方法
原型继承优化 中间件继承
原型继承,有多余的属性
要得到一个对象
// 黄种人
function YellowPerson(name, age, hobby) { // this -> new YellowPerson() 创建的实例化对象
this.hobby = hobby;
}
// 创建一个中间函数
var Fn = function () { } // var obj this->obj return this
Fn.prototype = Person.prototype;
// new Fn(); // {__proto__:Person.prototype}
YellowPerson.prototype = new Fn();
// 方便理解
// YellowPerson.prototype = {
// __proto__: Person.prototype,
// }
组合继承
构造函数继承 + 原型继承
优点
缺点
-
多了两个属性name,age
-
没有构造器
-
优化
-
// 人
function Person(name, age) { // 构造函数中 实例的属性和方法
// var obj = new Object(); this->构造函数Person创建的实例化对象
this.name = name;
this.age = age;
this.species = "human";
// this.__proto__ = Person.prototype
// return this;
}
// 原型对象上的属性和方法
Person.prototype.say = function () {
console.log(this.name, this.age);
}
Person.prototype.skill = function () {
console.log("thinking");
}
// 黄种人
function YellowPerson(name, age, hobby) { // this -> new YellowPerson() 创建的实例化对象
Person.call(this, name, age); // 构造函数的继承
this.hobby = hobby;
}
// 创建一个中间函数
var Fn = function () { } // var obj this->obj return this
Fn.prototype = Person.prototype;
// new Fn(); // {__proto__:Person.prototype}
YellowPerson.prototype = new Fn();
// 方便理解
// YellowPerson.prototype = {
// __proto__: Person.prototype,
// }
ES6的继承
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
//Person
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.species = "human";
}
skill() {
consoel.log("think");
}
say() {
console.log(this.name, this.age);
}
}
//YellowPerson
class YellowPerson extends Person {
// 默认继承父类构造器的属性和方法 以及原型对象上的属性和方法
constructor(name, age, hobby) {
super(name, age); // super 作为一个函数使用代表调用父类构造函数 相当于 ES5 Person.call(this,name,age)
this.hobby = hobby; // 拓展自己的实例属性 / 方法
}
// 拓展自己的YellowPerson 的动态属性和方法
// skin = "yellow";
speak() {
console.log("chinese");
}
// 改写父类
say() {
console.log(this.name, this.age, this.hobby);
}
}
YellowPerson.prototype.skin = "yellow";
简单继承
只是单纯的继承父类构造器的属和方法 以及原型对象上的属性和方法
class Person{
constructor(name, age){
this.name= name;
this.age= age;
}
say(){
console.log(this.name,this.age);
}
skill(){
console.log("thinking")
}
}
Person.prototype.species = "human";
// 最简单的继承 (只是单纯的继承父类构造器的属和方法 以及原型对象上的属性和方法)
class YellowPerson extends Person {
// 默认继承父类构造器的属和方法 以及原型对象上的属性和方法
// 缺点 没有自己的属性和方法
}
console.dir(YellowPerson);
var zhang = new YellowPerson("张三", 22);
console.log(zhang);
复杂继承
不仅继承自己的,还继承父类的
class Person{
constructor(name, age){
this.name= name;
this.age= age;
}
say(){
console.log(this.name,this.age);
}
skill(){
console.log("thinking")
}
}
Person.prototype.species = "human";
// 不仅继承父类的 还要有自己的
class YellowPerson extends Person {
// 定义自己的 实例的属性和方法
constructor (name,age,hobby){
// 作为一个函数使用 代表 调用父类构造函数 Person.call(this,name,age)
super(name,age);
this.hobby = hobby;
}
// 定义自己的 原型对象的方法
speak() {
console.log("chinese");
}
}
YellowPerson.prototype.skin = "yellow";
var zhang = new YellowPerson("张三", 22,"code");
console.log(zhang);
super的用法归纳(了解)
1. super 作为一个函数使用 代表 调用父类构造函数 (作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。)
2. super 作为一个对象使用
2.1 super作为一个对象 取值的时候 super 等价于 父类的原型对象 (此案例就相当于 Person.prototype)
2.2 super作为一个对象 赋值的时候 super 等价于 this
class YellowPerson extends Person {
constructor(name, age, hobby) {
// 1. super 作为一个函数使用
super(name, age); // 调用父类构造函数 ES5 Person.call(this,name,age)
this.hobby = hobby;
console.log(super.skill);
console.log(super.skill === Person.prototype.skill); //
this.x = "hello";
super.x = 123123;
}
// 拓展自己的动态方法
speak() {
console.log("chinese");
}
// 改写父类
say() {
console.log(this.name, this.age, this.hobby);
}
}
var zhang = new YellowPerson("张三", 18, "跳");
console.log(zhang);
zhang.say();
super用法
- 作为函数调用
- 作为一个函数使用代表调用父类构造函数 Person.call(this,name,age)
- 作为对象使用
- 取值时,等价于,父类原型对象 Person.prototype
- 赋值时,super作为一个对象,等价于this
面向对象之多态
多态 不同类(构造函数)的对象 在调用同一个方法 会显示不同的结果
JS 无态,天生就支持多态
class Cat {
constructor(name, color) {
this.name = name;
this.color = color;
}
call() {
console.log("喵喵");
}
}
class Dog {
constructor(name, color) {
this.name = name;
this.color = color;
}
call() {
console.log("旺旺");
}
}
class Pig {
constructor(name, color) {
this.name = name;
this.color = color;
}
call() {
console.log("哼哼");
}
}
var kitty = new Cat("Kitty", "pink");
var wang = new Dog("旺财", "black");
var pei = new Pig("佩奇", "pink");
function AnimallCall(animal) {
animal.call();
}
AnimallCall(kitty);
AnimallCall(wang);
AnimallCall(pei);