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' ]
//例一:
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 要看到这句先把上面的错误注释掉,否则程序一直卡在错误处不继续运行了。
。。。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现