JS中面向对象编程(OOP)的基本原理——this关键字、原型链、构造函数、继承、Mixin、闭包、IIFE

面向对象编程(Object Oriented Programming),是软件开发过程的主要方法之一。在OOP中,使用对象组织代码来描述事物及其功能。

虽然点符号是访问对象属性的有效方法(如myobj.name),但存在一个缺陷,如果变量名(如myobj)更改,则引用原始名称的任何代码都需要更新。如果一个对象对其属性有许多引用,则大概率会出错。这时,使用this关键字可以很好地避免这样的问题。

let duck = {
  name: "Aflac",
  numLegs: 2,
  //sayName: function() {return "The name of this duck is " + duck.name + ".";  
  //改用下面这句:
  sayName: function() {return "The name of this duck is " + this.name + ".";}
};

 在当前上下文中,this是指方法关联的对象:duck。如果对象的名称更改为mallard,无需在代码中找到所有duck改为mallard程序也能正常运行,this使代码可重用、易于阅读。

构造函数是创建新对象的函数它定义属于新对象的属性和行为

编写构造函数要遵循一些约定:

1)构造函数是用首字母大写的名称定义的,以区别于非构造函数的其他函数。

2)构造函数使用关键字this设置要创建的对象的属性。构造函数中的“this”总是指正在创建的对象。

3)构造函数定义属性和行为,而不是像其他函数那样返回值。

使用new运算符调用构造函数(如下所示),' new '告诉JavaScript创建一个名为blueBird的新Bird实例。如果没有new操作符,构造函数中的这一操作将不会指向新创建的对象,从而产生意外的结果。现在,blueBird拥有Bird构造函数中定义的所有属性。与任何其他对象一样,可以访问和修改blueBird的属性。

function Bird() {
  this.name = "Albert";
  this.color  = "blue";
  this.numLegs = 2;
}

let blueBird = new Bird();

让构造函数接受参数会更灵活:

function Dog(name,color) {
  this.name=name;
  this.color=color;
  this.numLegs=4;

}
let terrier=new Dog('haha','yellow')
console.log(terrier); //{ name: 'haha', color: 'yellow', numLegs: 4 }

现在可以在创建每个Dog时为其定义属性,这是JavaScript构造函数非常有用的一种方式,基于共享的特征和行为将对象组合在一起,并定义一个自动创建它们的蓝图。

每当构造函数创建一个新对象时,该对象就称为其构造函数的实例。JavaScript提供了一种使用instanceof操作符验证这一点的便捷方法。instanceof允许我们将对象与构造函数进行比较,根据该对象是否是使用某构造函数创建的,返回true或false。例子:

function House(numBedrooms) {
  this.numBedrooms = numBedrooms;
}
let myHouse=new House(99);
console.log(myHouse instanceof House); //true

自有属性

自有属性直接在对象实例本身上定义。看例子:

function Bird(name) {
  this.name = name;
  this.numLegs = 2;
}

name和numLegs被称为自有属性(own properties),因为是直接在实例对象上定义的。Bird的每个实例都各自拥有这些属性副本。以下代码将实例canary自有的所有属性添加到数组ownProps中:

复制代码
function Bird(name) {
  this.name = name;
  this.numLegs = 2;
}

let canary = new Bird("Tweety");
let ownProps = [];
for(let prop in canary){
  if(canary.hasOwnProperty(prop)){
    ownProps.push(prop);
  }
}
console.log(ownProps); //[ 'name', 'numLegs' ]
复制代码

原型属性

上例中,numLegs是固定不变的,对于Bird的所有实例,numLegs都可能具有相同的值,因此在每个Bird实例中实际上都有一个重复的变量numLeg。当有数百万个实例的话,就会有很多重复的变量,这些重复代码非常冗余。

此时使用Bird.prototype可以解决问题。原型(prototype)中的属性在Bird的所有实例中共享原型属性(prototype properties)是在原型上定义的。以下是如何将numLegs添加到Dog原型中:

function Dog(name) {
  this.name = name;
}
Dog.prototype.numLegs=4;//将numLegs添加到Dog原型中
//调用:
let beagle = new Dog("Snoopy");
console.log(beagle); //{ name: 'Snoopy' }
console.log(beagle.numLegs); //4

beagle的原型是Dog构造函数的一部分,写作Dog.prototype。由于所有实例都自动在原型(prototype)上拥有属性,所以可以将原型视为创建对象的“配方”。JavaScript中几乎每个对象都有一个prototype属性,该属性是创建它的构造函数的一部分。

分别遍历出所有的自有属性和原型属性

复制代码
function Dog(name) {
  this.name = name;
}
Dog.prototype.numLegs = 4;

let beagle = new Dog("Snoopy");
let ownProps = [];
let prototypeProps = [];
for(let prop in beagle){
  if(beagle.hasOwnProperty(prop)){
    ownProps.push(prop);
  }
  else{
    prototypeProps.push(prop);
  }
}
console.log(ownProps); //[ 'name' ]
console.log(prototypeProps); //[ 'numLegs' ]
复制代码
如果原型要添加很多属性咋办,总不能一个个单独添加吧?这时我们可以将原型设置为已包含属性的新对象。这样,属性将一次全部添加:
复制代码
function Dog(name) {
  this.name = name;
}
//将原型设置为已包含属性的新对象,属性将一次性全部添加:
Dog.prototype = {
  numLegs:4,
  eat:function(){},
  describe:function(){}
};
//现在我们来打印看看原型属性有哪些
let myDog=new Dog('wang');
let prototypeArr=[];
for(let gouzi in myDog) {
  if(myDog.hasOwnProperty(gouzi)){
    console.log('吃瓜群众');
  }
  else {
    prototypeArr.push(gouzi);
  }
}
console.log(prototypeArr); //[ 'numLegs', 'eat', 'describe' ]
复制代码
 
构造器属性
对象实例上都会有一个特殊的构造器属性(constructor property),构造器属性是对创建实例的构造函数的引用。构造器属性的优点是可以检查此属性,以确定它是什么类型的对象。看个例子就明白此属性的用法了:
复制代码
//例一:
let duck = new Bird(); let beagle = new Dog(); console.log(duck.constructor === Bird); //true console.log(beagle.constructor === Dog); //true //例二: function joinBirdFraternity(candidate) { if (candidate.constructor === Bird) { return true; } else { return false; } }
复制代码

注:由于构造器属性可以被覆盖(下面会介绍),因此通常最好使用instanceof方法检查对象的类型!

上面我们讲可以将原型设置为新对象,但这会导致构造器属性被擦除(覆盖)!这时检查对象类型就会出错:

复制代码
function Dog(name) {
  this.name = name;
}
Dog.prototype = {
  numLegs: 4,
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name);
  }
};
//测试:
let gouzi=new Dog('wangcai');
//检查对象的类型:
console.log(gouzi.constructor===Dog);//false,本来应该是true的。
console.log(gouzi.constructor === Object); //true
console.log(gouzi instanceof Dog); //true,用instanceof就不会出错。
复制代码

要解决此问题,我们需要在将原型设置为新对象时,同时定义构造器属性:

复制代码
function Dog(name) {
  this.name = name;
}
Dog.prototype = {
  constructor:Dog,//设置原型对象时,同时定义构造器属性:
  numLegs: 4,
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name);
  }
};
//测试:
let gouzi=new Dog('wangcai');
//检查对象的类型:
console.log(gouzi.constructor===Dog);//true
console.log(gouzi.constructor === Object); //false
console.log(gouzi instanceof Dog); //true
复制代码

对象直接从创建它的构造函数那里继承原型。下面例子中,beagle继承了Dog构造函数的原型,可以使用 isPrototypeOf 方法显示此关系:

function Dog(name) {
  this.name = name;
}

let beagle = new Dog("Snoopy");
console.log(Dog.prototype.isPrototypeOf(beagle));//true

原型链

JavaScript中的所有对象(少数例外)都有一个原型。此外,对象的原型本身就是一个对象。因为原型是一个对象,所以原型可以有自己的原型!

这有什么用处呢?

还记得hasOwnProperty方法吗?hasOwnProperty方法是在Object.prototype中定义的,为什么任何对象都可以使用hasOwnProperty方法?就是因为原型链!

原型链示例:通过Dod.prototype可以访问Object.prototype,然后通过beagle可以访问Dod.prototype。在这个原型链中,Dog是beagle的父类型(supertype),beagle是子类型(subtype),Object是Dog和beagle的父类型。注意,Object也是JavaScript中所有对象的父类型(因此任何对象都可以使用hasOwnProperty方法哈哈):

复制代码
function Dog(name) {
  this.name = name;
}

let beagle = new Dog("Snoopy");
console.log(beagle.hasOwnProperty('name'));//true
console.log(Dog.prototype);//{}
console.log(typeof Dog.prototype);//object
console.log(Dog.prototype.isPrototypeOf(beagle));//true
console.log(Object.prototype.isPrototypeOf(Dog.prototype));//true
复制代码

编程中有一个原则叫做不要重复自己(Don't Repeat Yourself( 简写为DRY )),重复代码之所以成为问题,是因为任何更改都需要在多个位置修复代码。这通常意味着更多的工作量和更多的犯错几率。

有重复代码的示例:

复制代码
function Cat(name) {
  this.name = name;
}

Cat.prototype = {
  constructor: Cat,
  eat: function() {
    console.log("nom nom nom");
  }
};

function Bear(name) {
  this.name = name;
}

Bear.prototype = {
  constructor: Bear,
  eat: function() {
    console.log("nom nom nom");
  }
};
复制代码

没有重复代码的示例(eat方法在两个地方重复。可以通过编辑代码创建名为Animal的超类型(或父类型),以遵循DRY原则):

复制代码
function Cat(name) {
  this.name = name;
}

Cat.prototype = {
  constructor: Cat
};

function Bear(name) {
  this.name = name;
}

Bear.prototype = {
  constructor: Bear
};

function Animal() { }

Animal.prototype = {
  constructor: Animal,
  eat:function(){console.log("nom nom nom");}
};
复制代码

上面创建了一个名为“Animal”的父类,它定义了所有动物共享的行为。现在看看如何从父类继承行为。

第一步是创建实例。使用new可以创建父类型(或父类型)的实例,但使用" let animal = new Animal(); "此语法进行继承时有一些缺点,我们可以使用另一个方法创建实例进行继承:" let animal = Object.create(Animal.prototype); ",Object.create(obj)创建一个新对象,并将obj设置为新对象的原型。通过将animal的原型设置为Animal的原型,可以有效地为animal实例提供与任何其他Animal实例相同的“配方”:

复制代码
function Animal() { }

Animal.prototype = {
  constructor: Animal,
  eat: function() {
    console.log("nom nom nom");
  }
};

let duck=Object.create(Animal.prototype); 
let beagle=Object.create(Animal.prototype);
console.log(duck.constructor===Animal);//true
console.log(beagle instanceof Animal);//true
复制代码

第二步是设置子类型的原型( 一个对象可以通过引用其原型对象从另一个对象继承其内容 )

语法句式:ChildObject.prototype = Object.create(ParentObject.prototype);

复制代码
function Animal() { }

Animal.prototype = {
  constructor: Animal,
  eat: function() {
    console.log("nom nom nom");
  }
};

function Dog() {this.name='xiaoxiao' }

Dog.prototype=Object.create(Animal.prototype);//设置子类型的原型,Dog是Animal的一个实例
let beagle = new Dog();//Dog是Animal的一个实例,beagle是Dog的实例,故beagle继承了Animal的所有属性,包括eat方法。 console.log(beagle.constructor===Animal);//true console.log(beagle.name);//xiaoxiao console.log(beagle.eat());//nom nom nom
复制代码

当一个对象从另一个对象继承其原型时,它也继承父类型的构造器属性。但duck和所有Bird的实例化表明它们是由Bird建造的,而不是Animal;beagle和所有Dog的实例化表明它们是由Dog建造的,而不是Animal。为此,可以手动将Bird的构造器属性设置为Bird对象、Dog的构造器属性设置为Dog对象:

复制代码
function Animal() { }
function Bird() { }
function Dog() { }

Bird.prototype = Object.create(Animal.prototype);
Dog.prototype = Object.create(Animal.prototype);

let duck = new Bird();
let beagle = new Dog();

//当一个对象从另一个对象继承其原型时,它也继承父类型的构造器属性:
console.log(duck.constructor);//[Function: Animal]
console.log(beagle.constructor);//[Function: Animal]

//手动将Bird的构造器属性设置为Bird对象、Dog的构造器属性设置为Dog对象:
Bird.prototype.constructor=Bird;
Dog.prototype.constructor=Dog;
console.log(duck.constructor);//[Function: Bird]
console.log(beagle.constructor);//[Function: Dog]
复制代码

除了从父类继承的内容之外,还可以添加子类对象特有的行为。语法句式为:ChildObject.prototype.methodName = function() {...};

跟构造器函数一样,直接将要添加的内容添加到Dog的原型中即可:

复制代码
function Animal() { }
Animal.prototype.eat = function() { return"nom nom nom"; };

function Dog() { }
Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor=Dog;
//跟构造器函数一样,直接将要添加的内容添加到Dog的原型中即可:
Dog.prototype.bark=function(){return'Woof!';}

let beagle = new Dog();
console.log(beagle);//{}
console.log(beagle.eat());//nom nom nom  
console.log(beagle.bark());//Woof!  
复制代码

我们可以对继承来的方法进行重写(覆盖)。在ChildObject.prototype添加与要重写的方法名称相同的方法名称即可:

复制代码
function Bird() { }

Bird.prototype.fly = function() { return "I am flying!"; };

function Penguin() { }
Penguin.prototype = Object.create(Bird.prototype);
Penguin.prototype.constructor = Penguin;
Penguin.prototype.fly=function(){return'Alas, this is a flightless bird.';}//重写方法fly

let penguin = new Penguin();
console.log(penguin.fly()); //Alas, this is a flightless bird.
复制代码

JavaScript在实例Penguin的原型链上查找方法的方式:

Penguin=> Is fly() defined here? Yes. Execute it and stop searching.

Bird => fly() is also defined, but JavaScript stopped searching before reaching this level.

Animal => JavaScript stopped searching before reaching this level.

Object => JavaScript stopped searching before reaching this level.

Mixin:对于不相关的对象(如鸟和飞机),继承并不是最好的解决方案,虽然它们都会飞,但鸟不是飞机,反之亦然。对于不相关的对象,最好使用Mixin。Mixin允许其他对象使用函数集合(对象作为参数被传递到Mixin,然后Mixin将自身包含的属性方法指定给每个对象):

复制代码
let bird = {
  name: "Donald",
  numLegs: 2
};
let boat = {
  name: "Warrior",
  type: "race-boat"
};
//Mixin可以看作一个函数集合,接受任何对象作为参数传入,传入的对象可以使用集合里的方法。
let glideMixin=function(obj){
  obj.glide=function(){
    console.log('glide down to the runway.');
  }
}
//对象bird和boat被传递到glideMixin,然后glideMixin将glide函数指定给每个对象。
glideMixin(bird);
glideMixin(boat);
//调用
bird.glide();//glide down to the runway.
boat.glide();//glide down to the runway.
复制代码

 闭包:构造函数里创建的变量,只有构造函数里的方法能访问(出了这个范围就无法访问,利用了代码块里变量的局部性),因为JS里函数能访问创建它的时候同级的上下文。

使用闭包的目的:保护对象内的属性不被外部修改。

如何建立闭包:在构造函数中创建变量,使属性私有化,将该变量的范围更改为在构造函数内,而不是全局可用。这样,变量只能由构造函数内的方法访问和更改。

复制代码
function Bird() {
  let weight=15
  this.getWeight=function(){
    return weight;
  }
}
let myBird=new Bird();
console.log(myBird.weight);//undefined
console.log(myBird.getWeight());//15
复制代码

立即调用函数表达式“ Immediately Invoked Function Expression (简写为IIFE) ”,语法句式为: ( 函数体 ) ( );

可以看到,函数没有名称,也没有存储在变量中。函数表达式末尾的括号“()”会导致函数立即被执行或调用,此模式称为立即调用函数表达式或IIFE:

复制代码
//例一:
(function () {
  console.log("Chirp, chirp!");
})();//控制台会立马输出Chirp, chirp!

//例二:
(function () {
  console.log("A cozy nest is ready");
})
();//控制台会立马输出A cozy nest is ready
复制代码

JS中会使用立即调用函数表达式(IIFE)创建模块,将相关函数分组到单个对象或模块中。上面创建的Mixin可以用IIFE全部打包给变量:

复制代码
function glideMixin(obj) {
  obj.glide = function() {
    console.log("Gliding on the water");
  };
}
function flyMixin(obj) {
  obj.fly = function() {
    console.log("Flying, wooosh!");
  };
}

//将Mixin打包到模块(就一个变量)里:
let motionModule = (function () {
  return {
    glideMixin: function(obj) {
      obj.glide = function() {
        console.log("Gliding on the water");
      };
    },
    flyMixin: function(obj) {
      obj.fly = function() {
        console.log("Flying, wooosh!");
      };
    }
  }
})();
复制代码

上面这个IIFE返回一个对象motionModule。这个返回的对象包含作为对象属性的所有mixin方法。模块模式的优点是,所有的行为(motion behaviors)都可以打包成一个对象,然后由代码的其他部分使用。下面是一个使用示例:

motionModule.glideMixin(duck);
duck.glide();

再看个例子:

复制代码
/*let isCuteMixin = function(obj) {
  obj.isCute = function() {
    return true;
  };
};
let singMixin = function(obj) {
  obj.sing = function() {
    console.log("Singing to an awesome tune");
  };
};
*/
//将上面的Mixin全部打包丢到funModule变量里,方便调用时使用。
let funModule=(
  function(){
    return {
      isCuteMixin:function(obj){obj.isCute=function(){return true;};},
      singMixin:function(obj){obj.sing=function(){console.log('Singing to an awesome tune');};}
    }
  }
)
();

//调用:
function Bird(){
  this.name='yanzi'
}
let myBird=new Bird();
funModule.isCuteMixin(myBird);
console.log(myBird.isCute());//true
//myBird.sing(); //报错:TypeError: myBird.sing is not a function
funModule.singMixin(myBird);
myBird.sing(); //Singing to an awesome tune 要看到这句先把上面的错误注释掉,否则程序一直卡在错误处不继续运行了。
复制代码

。。。

 

posted @   枭二熊  阅读(233)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示