JS高级-语言特性
JS高级-语言特性
一、JS面向对象编程
1.1面向对象介绍
1.2面向对象编程
1.3创建对象
二、构造函数
2.1构造函数
2.2构造函数存在的问题
三、原型
3.1构造函数的prototype属性
3.2构造函数、实例、原型三者之间的关系
3.3原型对象的获取及修改
3.4原型及原型链
3.5更简单的原型语法
3.6原生对象的原型
3.7原型对象的问题及使用建议
一、JS面向对象编程
1.1面向对象介绍
什么是对象?
Everything is object (万物皆对象), JS语言中将一切都视为 对象
- 对象是对概念的具体化体现:
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。
当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
- 编程中对象是一个容器,封装了属性(property)和方法(method)
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
也可以将其简单理解为:数据集或功能集。
ECMAScript 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都 映射到一个值。
1.2面向对象编程
面向过程:以前写js代码,都是面向过程。
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
典型问题:将大象关进冰箱分几步
面向过程式:
开门(冰箱)
放进(冰箱,大象)
关门(冰箱)
面向对象式:
两个对象:大象、冰箱
冰箱.开门()
冰箱.放进(大象)
冰箱.关门()
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性:
- 封装性:对象中的属性、方法,对外提供一组方法(操作数据的接口),使用时无需关心内部具体实现。
- 继承性:将同一类对象,公共的属性方法,提取到一个单独公共对象中,具体的子对象可以使用这个公共对象中的成员
- [多态性]:动物的叫声为例,同一类的对象,有相同的方法(动物会叫), 但是每个具体的对象,方法实现的效果不一样(每个动物叫声不一样)
1.3创建对象
JavaScript 语言(ES5)的对象体系,不基于“类” 创建对象,是基于构造函数(constructor)和原型链(prototype)。
简单方式创建对象
我们可以直接通过 new Object() 创建:
var person = new Object()
person.name = 'Jack'
person.age = 18
person.sayName = function () {
console.log(this.name)
}
字面量方式创建对象
每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:
var person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?
var person1 = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
var person2 = {
name: 'Mike',
age: 16,
sayName: function () {
console.log(this.name)
}
}通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
简单方式的改进:工厂函数
我们可以写一个函数,解决代码重复问题:
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}然后生成实例对象:
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但是这依然没有脱离 使用 字面量方式创建对象 的本质;
二、构造函数
2.1构造函数
JavaScript 语言使用构造函数作为对象的模板。 所谓 ”构造函数”,就是一个普通的函数,只不过我们专门用它来生成对象(new 构造函数),这样使用的函数,就是构造函数;
它提供模板,描述对象的基本结构。 一个构造函数,可以生成多个对象,这些对象都有相同的结构。
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var p1 = new Person('Jack', 18)
p1.sayName() // => Jack
var p2 = new Person('Mike', 23)
p2.sayName() // => Mike
解析 构造函数代码 的执行
在上面的示例中,Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。 这是为什么呢?
我们注意到,Person() 中的代码与 createPerson() 有以下几点不同之处:
- 没有显式的创建对象(没有使用字面量)
- 直接将属性和方法赋给了 this
- 没有 return 语句
- 函数名使用的是大写的 Person
而要创建 Person 实例,则必须使用 new 操作符。
以这种方式调用构造函数会经历以下 5 个步骤:
创建一个空对象,作为将要返回的对象实例。
将这个空对象的原型,指向构造函数的prototype属性。先记住,后面讲
将这个空对象赋值给函数内部的this关键字。
执行构造函数内部的代码。
返回新对象
function Person (name, age) {
// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
// 然后让内部的 this 指向新创建的对象
// 接下来所有针对 this 的操作实际上操作的就是刚创建的这个对象
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将 this 返回,也就是这个新对象
}
var per= new Person ('张三',30);
构造函数和实例对象的关系
构造函数是根据具体的事物抽象出来的抽象模板
实例对象是根据抽象的构造函数模板得到的具体实例对象
实例对象由构造函数而来,一个构造函数可以生成很多具体的实例对象,而每个实例对象都是独一无二的;
每个对象都有一个 constructor 属性,该属性指向创建该实例的构造函数
反推出来,每一个对象都有其构造函数
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true因此,我们可以通过实例对象的 constructor 属性判断实例和构造函数之间的关系
2.2构造函数存在的问题
以构造函数为模板,创建对象,对象的属性和方法都可以在构造函数内部定义;
function Cat(name, color) {
this.name = name;
this.color = color;
this.say = function () {
console.log('hello'+this.name,this.color);
};
}
var cat1 = new Cat('猫', '白色');
var cat2 = new Cat('猫', '黑色');
cat1.say();
cat2.say();
在该示例中,从表面上看好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象, name 和 say 都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function say(){
console.log('hello'+this.name,this.color);
}
function Cat(name, color) {
this.name = name;
this.color = color;
this.say = say;
}
var cat1 = new Cat('猫', '白色');
var cat2 = new Cat('猫', '黑色');
cat1.say();
cat2.say();
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局变量(函数名)冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局变量(函数名)冲突的问题:
var s = {
sayhello:function (){
console.log('hello'+this.name,this.color);
},
saycolor:function(){
console.log('hello'+this.color);
}
}
function Cat(name, color) {
this.name = name;
this.color = color;
this.sayhello = s.sayhello;
this.saycolor = s.saycolor;
}
var cat1 = new Cat('猫', '白色');
var cat2 = new Cat('猫', '黑色');
cat1.sayhello();
cat2.saycolor();
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。 但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
三、原型
3.1构造函数的prototype属性
JavaScript 的每个对象都继承另一个父级对象,父级对象称为 原型 (prototype)对象。
原型也是一个对象,原型对象上的所有属性和方法,都能被子对象 (派生对象) 共享 通过构造函数生成实例对象时,会自动为实例对象分配原型对象。 而每一个构造函数都有一个prototype属性,这个属性就是实例对象的原型对象。
null没有自己的原型对象。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在构造函数的 prototype 属性上,
也就是实例对象的原型对象上。
function Cat(color) {
this.color = color;
}
Cat.prototype.name = "猫";
Cat.prototype.sayhello = function(){
console.log('hello'+this.name,this.color);
}
Cat.prototype.saycolor = function (){
console.log('hello'+this.color);
}
var cat1 = new Cat('白色');
var cat2 = new Cat('黑色');
cat1.sayhello();
cat2.saycolor();
这时所有实例的 name 属性和 sayhello() 、saycolor 方法, 其实都是同一个内存地址,指向构造函数的 prototype 属性,因此就提高了运行效率节省了内存空间。
3.2构造函数、实例、原型三者之间的关系
构造函数的prototyp属性,就是由这个构造函数new出来的所有实例对象的 原型对象
前面已经讲过,每个对象都有一个 constructor 属性,该属性指向创建该实例的构造函数
对象.__proto__ (两边都是两个下划线):获取对象的原型对象;
console.log(cat1.__proto__ == Cat.prototype); // true
注意:ES6标准规定,__proto__属性只有浏览器环境下才需要部署,其他环境可以不部署,因此不建议使用
3.3原型对象的获取及修改
上节可以看到,想要获取一个实例对象的原型对象,有两种方式:
1:通过实例对象的构造函数的prototype属性获取: 实例对象.constructor.prototype
2:通过实例对象的 __proto__ 属性获取: 实例对象.__proto__
而这两种方式,我们都不建议使用:
obj.constructor.prototype在手动改变原型对象时,可能会失效。
function P() {};
var p1 = new P();
function C() {};
// 修改构造函数的prototype属性的值为p1
C.prototype = p1; //也就是说,此后所有有C构造函数得到的对象的原型对象都是p1;
var c1 = new C();
console.log(c1.constructor.prototype === p1) // false
推荐设置获取实例对象的原型的方式:
Object.getPrototypeOf(实例对象) 方法返回一个对象的原型对象。
这是获取原型对象的标准方法。
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('猫'); //获取cat1对象的原型对象
var s = Object.getPrototypeOf(cat1);
console.log(s);
Object.setPrototypeOf(实例对象,原型对象) 为现有对象设置原型对象 第一个是实例对象,第二个是要设置成为实例对象的原型对象的对象 这是设置原型对象的标准方法。
function Cat(name) {
this.name = name;
}
var ob = {p:'波斯'};
var cat1 = new Cat('猫');
//设置cat1的原型对象为ob
Object.setPrototypeOf(cat1,ob);
console.log(cat1.p);//cat1的原型对象中有p属性
console.log(Object.getPrototypeOf(cat1));
console.log(cat1.__proto__);
//注意:如果对象的原型被改变,不会影响构造函数获取的原型的结果
console.log(Cat.prototype == cat1.__proto__); //false
以上的两种方法,都是在ES6新标准中添加的;
重要图示
3.4原型及原型链
所有对象都有原型对象;
function Cat(name, color) {
this.name = name;
}
var cat1 = new Cat('猫');
console.log(cat1.__proto__.__proto__.__proto__);而原型对象中的属性和方法,都可以被实例对象直接使用;
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
- 如果还是找不到,就到原型的原型去找,依次类推。
- 如果直到最顶层的Object.prototype还是找不到,则返回undefined。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
对象的属性和方法,有可能是定义在自身内,也有可能是定义在它的原型对象上。 由于原型本身也是对象,又有自己的原型,所以形成了一条 原型链(prototype chain)。
3.5更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 构造函数.prototype 。 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
在该示例中,我们将 Person.prototype 重置到了一个新的对象。 这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员(构造函数)。
所以,我们为了保持 constructor 的指向正确,建议的写法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
// 将这个对象的构造函数指向Person
//constructor: Person, // => 手动将 constructor 指向正确的构造函数
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
var p = new Person();
3.6原生对象的原型
所有构造函数都有prototype属性;
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- ……
为内置对象扩展原型方法:
例:
var ar = [1,5,23,15,5];
//获取数组中小于10的数
function f(){
var minarr = [];
this.forEach(function(v,k){
if(v<10){
minarr.push(v);
}
})
return minarr;
}
Object.getPrototypeOf(ar).min10 = f;
console.log(ar.min10());//[1, 5, 5]
// 其他数组对象也具有相应的方法
var a = [1,2,34,7];
console.log(a.min10()); //[1, 2, 7]
这种技术被称为猴子补丁,并且会破坏封装。尽管一些流行的框架(如 Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。
3.7原型对象的问题及使用建议
性能问题:
在原型链上查找属性时是比较消耗资源的,对性能有副作用,这在性能要求苛刻的情况下很重要。
另外,试图访问不存在的属性时会遍历整个原型链。
//声明构造函数Man
function Man(name){
this.name = name;
this.p = function(){
console.log(this.name+'跑');
}
}
var m = new Man('张三');
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('age')); //false
hasOwnProperty 是 JavaScript 中唯一处理属性并且不会遍历原型链的方法。
注意:检查属性是否undefined还不够。该属性可能存在,但其值恰好设置为undefined。
//声明构造函数Man
function Man(name){
this.name = name;
this.n = undefined;
this.p = function(){
console.log(this.name+'跑');
}
}
var m = new Man('张三');
if(m.n == undefined){
console.log('没有n属性')
}
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('name')); // true
console.log(m.hasOwnProperty('n')); //true