面向对象的语言都有一个标志,那就是他们都有类的概念。
而通过类可以创建任意多个具有相同属性和方法的对象。
JavaScript中对象被定义为“无序属性的集合,其属性值可以包含基本值,对象或者函数”
创建对象
Object构造函数模式
- 先创建空object对象,再动态添加属性和方法
- 使用场景:起始不确定对象内容的数据
创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它添加属性和方法。
var person = new Object()
person.name = 'Tom'
person.age = 29
person.sayName = function(){
alert(this.name)
}
早期的开发人员经常使用这个模式创建新对象。
对象字面量模式
:::tip
现在对象字面量成为创建对象这种对象的首选模式
在对象里面this就是对象本身
:::
- 使用{}创建对象同时指定属性方法
- 使用场景:起始确定对象内容的数据
- 问题:如果创建多个对象 代码很重复
var person = {
name:'Tom',
age:29,
sayName:function(){
alert(this.name)
}
}
对象属性的访问
obj.prop
var person = {
name:'Tom',
age:29,
sayName:function(){
alert(this.name)
}
}
person.name
obj[‘prop’]
- 属性名中包含特殊字符时需要以中括号访问
var p={} //p.content-type='text/json' p['content-type']='text/json'
- 变量名不确定
var propName = 'myAge' var value = 18 //p.propName=value p[propName]=value
对象属性的删除
var person = {
name:'Tom',
age:29,
sayName:function(){
alert(this.name)
}
}
delete person.name
delete person.sayName
delete person.sayName() //这个是方法的执行不是删除
属性类型
ECMAScript有两种属性:数据属性,访问器属性
数据属性
数据属性有4个可以描述其行为的特性
- [[Configurable]]:能否通过delete删除属性而重新定义属性,是否可配置
- [[Enumerable]]:能否通过for-in循环返回属性
- [[Writable]]:能否修改属性的值
- [[Value]]:属性的数据值
对于前面例子中那样直接在对象上定义的属性,他们的[[Configurable]],[[Enumerable]],[[Writable]]都是true,而[[Value]]被设置为指定的值(如果不指定,为undefined)
var person = {
name:'Tom' //这里创建了一个name属性,[[Value]]值为Tom
}
要想修改属性默认的特性,必须使用Object.defineProperty()方法。
这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。
其中描述符descriptor对象的属性必须是:configurable,enumerable,writable和value
var person = {}
Object.defineProperty(
person,
"name",
{
configurable:false,//这个name属性值不可配置
//configurable为false,表示不能从对象中删除删除属性,不能对它调用delete。而且一旦属性定义为不可配置就不能再把它变回可配置了。此时再调用Object.defineProperty方法修改除writable之外的特性都会导致错误
//也就是说可以多次调用Object.defineProperty方法修改同一个属性,但当configurable=false后就有了限制
writable:false,//这个name属性值不可修改
value:'Tom'
}
)
访问器属性
访问器属性不包含数据值,它是一对getter,setter函数(不过这两个函数都不是必须的)。
访问器属性有如下4个特性
- [[Configurable]]:能否通过delete删除属性而重新定义属性,是否可配置
- [[Enumerable]]:能否通过for-in循环返回属性
- [[Get]]:在读取属性时调用的函数 默认值undefined
- [[Set]]:在写入属性时调用的函数 默认值undefined
访问器属性不能直接定义,必须通过Object.defineProperty来定义
var book = {
_year:2004,
last:0
}
Object.defineProperty(
book,
"year",
{
get:function(){
return this._year
},
set:function(newValue){
this.last = this._year
this._year = newValue
}
}
)
book.year = 2005
console.log(book.year)
//对象_year前面的下划线是一种记号,表示只能通过对象方法访问的属性
//访问器属性year则包含一个getter和一个setter,用于修改对象中的_year
//这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化
//不一定非要同时指定getter和setter
//只知道getter意味属性是不能写的
Object.defineProperty是ES5的方法,在这个方法之前,要创建访问器属性,一般都使用两个非标准的方法 defineGetter,defineSetter
var book = {
_year:2004,
last:0
}
book.__defineGetter__("year",function(){
return this._year;
})
book.__defineSetter__("year",function(){
this.last = this._year
this._year = newValue
})
定义多个属性
ES5还定义了一个Object.defineProperties()方法。
利用这个方法可以通过描述符一次定义多个属性。
var book = {}
Object.defineProperties(book,{
_year:{
value:2004
},
last:{
value:0
},
year:{
get:function(){
return this._year
},
set:function(newValue){
this.last = this._year
this._year = newValue
}
}
})
/*
以上代码在book对外定义了两个数据属性(_year,last)
一个访问器属性(year)
*/
读取属性的特性
使用ES5的Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符。
这个方法接收两个参数,属性所在对象和读取其描述符的属性名称。
返回值是一个对象,可以是访问器属性集合对象,或者是数据属性集合对象
var book = {}
Object.defineProperties(book,{
_year:{
value:2004
},
last:{
value:0
},
year:{
get:function(){
return this._year
},
set:function(newValue){
this.last = this._year
this._year = newValue
}
}
})
var desc = Object.getOwnPropertyDescriptor(book,"_year");
console.log(desc.value) //2004
console.log(desc.configurable) //false
var test = {name:'Tom'}
Object.getOwnPropertyDescriptor(test,'name')
//{value: "Tom", writable: true, enumerable: true, configurable: true}
创建对象的优化
虽然Object构造函数或对象字面量都可以用来创建单个对象,
但这些方法有个明显的缺点:使用同一个接口创建很多对象时会产生大量的重复代码。
为了解决这个问题,人们开始使用工厂模式的一些变体
工厂模式
在ES中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的实例
function createPerson(name){
var o = new Object()
o.name = name
o.sayName = function(){
alert(this.name)
}
return o
}
var person1 = createPerson('Tom')
var person2 = createPerson('Jank')
:::warning
- 工厂模式虽然解决了创建多个相识对象的问题,
- 但没有解决对象识别的问题(即怎样知道一个对象的类型,它创建的对象没有一个具体的类型,都是object类型)
- 于是出现了新的创建对象的模式
:::
构造函数模式
function Person(name){
this.name = name
this.sayName = function(){
alert(this.name)
}
}
var person1 = new Person('Tom')
var person2 = new Person('Jank')
:::tip
- 可以发现构造函数的一些特定:
- 用new显示地创建了对象
- 直接将属性和方法赋值给了this
- 没有return语句
- 函数名大写(按照惯例构造函数始终以一个大写字母开头,非构造函数以小写开头,这个做法借鉴了其他OO语言)。
:::
构造函数传参最佳实践
//1. 对象字面量 直接 创建对象 对象的属性可以修改 添加 删除
var obj ={
name:'kitty',
age:16,
eat:function(){
}
}
delete obj.name
delete obj.eat//不能是delete obj.eat()
//2. 用构造函数创建对象
//与字面量创建一样,没有任何优势
var obj = new Object()
……
//3. 自定义构造函数
//大驼峰 => 构造函数
function Teacher(name,sex){
this.name=name,
this.sex=sex,
this.smoke =function(){
console.log('I am smoking')
}
}
/**
函数没执行的时候 this 还没有生成
它是构造函数,不是对象
只有实例化的构造函数才是对象,this指向这个实例化的对象
**/
var teacher1 = new Teacher('张三','男')
var teacher2 = new Teacher('李四','女')
- 最佳实战:使用构造函数时传递对象参数
//3.1使用构造函数时传递对象参数(最佳实战)
function Teacher(obj){
this.name=obj.name,
this.sex=obj.sex,
this.smoke =function(){
console.log('I am smoking')
}
}
//new vue() 就是按这种方法初始化的,所以封装框架的时候也按此方法
//易于维护,适合框架的配置。属性名和属性值可以一目了然,强烈推荐
//也没有参数顺序的问题
var teacher1 = new Teacher({
name:'张三',
sex:'男'
})
//也可以在html中配置,把对象字符串解析为对象
<div data-config='{name:"张三"}'></div>
new(this)操作符-构造函数实例化原理
:::tip new的作用
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
:::
function Car(color,brand){
// 1. this = {} 不用this 照样可以
// 2. 执行代码
this.color=color
this.brand = brand
// 3. 函数最后有一句隐式的 return this
}
var car = new Car('red','Mazda')
// ==============
function Car(color,brand){
var me = {}
me.color=color
me.brand = brand
return me //显式 return
}
var car = new Car('red','Mazda')
//总结:new的时候 把函数里面指向window的this,转换为指向为实例化的对象
:::warning
- 如果return的是非引用值,它不会识别 仍然隐式返回 this
- 如果return 引用值 ,则正确返回引用值
:::
function Car(color,brand){
this.color=color
this.brand = brand
return '123'
//如果return的是非引用值,它不会识别 仍然隐式返回 this
//如果return 引用值 ,则正确返回
}
var car = new Car('red','Mazda')//123
构造函数属性constructor和instanceof
同类型对象的实例都有一个constructor构造函数属性,它指向构造函数
person1.constructor == Person//true
person2.constructor == Person//true
对象的constructor属性最初是用来标识对象类型的。
但是检测对象类型,还是instanceof操作符更可靠一些。
上面创建的对象都是Object的实例,
同时也是Person的实例
person1 instanceof Object // true
person1 instanceof Person // true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
而这正是构造函数胜过工厂模式的地方
将构造函数当做函数
构造函数与其他函数的唯一区别,就在于它的调用方式的不同。
任何函数只要通过new操作符来调用,那么它就可以作为构造函数
任何函数如果不通过new操作符调用,那么它和普通函数就没有什么区别
//-----------
function Car(color,brand){
//如果前面有new ,this = {} 指向当前实例
this.color=color
this.brand = brand
//函数最后有一句隐式的 return this
}
Car() //执行时 this 指向window
new Car() //执行时 this 指向new出来的实例变量
构造函数的问题
//-----------
function Person(name){
this.name = name
this.sayName = new Function("alert(this.name)")
//与声明函数在逻辑上是等价的
}
从这个角度看构造函数,发现每个构造函数的实例的Function实例都不相同。
因此不同实例上的同名函数都不相等
但每个实例上的同名function所完成的功能都是一样,没必要多此创建。
person1.sayName == person2.sayName //falses
通过把函数定义转移到构造函数外部来解决这个问题
//-----------
function Person(name){
this.name = name
this.sayName = sayName
//与声明函数在逻辑上是等价的
}
function sayName(){
alert(this.name)
}
这样确实可以解决了对象实例同名函数每次都重新创建的问题。
可是新的问题又来了:
在全局作用域中定义的函数只能在某个构造函数中使用,让全局函数名不副实
如果对象需要定义很多方法,那就丝毫没有封装性可言
不过这些可以通过原型来解决
包装类
包装类就是构造函数
// 原始值是没有属性的
var a = 123
a.len = 3
// new Number(123).len = 3 ;delete; 包装类的过程->赋值后但没有保存
console.log(a.len) // undefined
var obj = 3;
new Number(obj).name = '123' // 包装类的过程->执行完后没有保存
console.log(obj.name)
var str = 'abc'
console.log(str.length) // 3
console.log(new String(str).length) //包装类的过程,字符串本身有这个属性
// 数组的截断方法
var arr = [1,2,3,4,5]
arr.length = 3;
console.log(arr)
// 字符串没有截断方法
function Obj(){}
Obj.prototype.num = 1;
var obj1 = Object.create(Obj.prototype)
var obj2 = new Obj()
// obj1 , obj2是一样的
// 创建obj空对象
var obj = Object.create(null)
obj.num = 1; // 不是所有的对象都继承Object.prototype
var obj1 = {
count:2
}
// 自己指定prototype是没有用的,我们只能修改它
obj.__proto__ = obj1;
console.log(obj,obj.count)
// undefined null 没有 toString 方法, 没有包装类
var num = 1;
num.toString()//包装类的原因
var num2 = new Number(num);
console.log(num2.toString());
var num = 1;
var obj = {};
var obj2 = Object.create(null);
document.write(num);
document.write(obj);
document.write(obj2); //报错,obj2 没有原型,也没有toString方法
Object.prototype.toString.call(1)
原型模式
理解原型对象
// 1. 给原型对象添加单个属性
function Person(){
// var this = {
// __proto__:Person.prototype
// }
}
Person.prototype.name = 'Tom'
Person.prototype.sayNam = function(){
alert(this.name)
}
var person1 = new Person()
console.log(person1)
// 打印出的对象有以下属性
// [[Prototype]]: Object
// name: "Tom"
// sayNam: ƒ ()
// constructor: ƒ Person()
// [[Prototype]]: Object
console.log(person1.__proto__)
// name: "Tom"
// sayNam: ƒ ()
// constructor: ƒ Person()
// [[Prototype]]: Object
// 2. 给原型对象添加多个属性,开发过程中经常这样写
function Person(){}
Person.prototype = {
name:'Tom',
sayNam:function(){
alert(this.name)
}
}
var person1 = new Person()
console.log(person1)
// =================
function Person(){}
Person.prototype.name = 'Tom'
Person.prototype.sayNam = function(){
alert(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.sayName == person2.sayName //true
::: tip 对于每个实例化的对象
- 每个实例化对象都有一个__proto__属性,这个属性指向当前对象实例的原型prototype
- 只有实例化的对象才会有原型
- 同类型不同实例的对象的原型都是一样的。
- 使用原型对象的好处是可以让所有对象实例共享原型所包含的属性和方法。
:::
::: tip 对于每个构造函数
- 构造函数的prototype属性
- 每个构造函数都有一个prototype属性,它默认指向该构造函数的原型对象
- 原型对象有一个属性constructor,它指向该构造函数
- 创建了自定义的构造函数之后,其原型对象默认只会取得函数对象本身的constructor属性;至于其他方法,则都是从Object继承而来的
- ES5管这个原型对象叫[[Prototype]],但它没有标准的访问,浏览器在每个实例对象上都支持一个属性__proto__ 。这个链接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间
- 给原型对象添加属性(一般都是方法)
作用:函数的所有实例对象自动拥有原型中的属性(方法)
:::
console.log(Date.prototype)
console.log(typeof Date.prototype) //"object"
function fun(){}
console.log(fun.prototype)
/*
{
constructor:fun(),
[[Prototype]]:Object
}
*/
//给原型对象添加属性-->实例对象可以访问
fun.prototype.test=function(){console.log("test()")}
console.log(fun.prototype)
var fun1 = new fun();
fun1.test()
//原型对象有一个构造函数属性constructor,它指向函数对象
console.log(Date.prototype.constructor===Date) //true
console.log(fun.prototype.constructor===fun) //true
console.log(fun.prototype.constructor===fun1.constructor) //true
//函数与原型对象相互引用
:::tip
- 给原型对象添加属性–>实例对象可以访问
- 原型对象有一个构造函数属性constructor,它指向函数对象
- 函数与原型对象相互引用
:::
:::warning
虽然可以通过对象实例访问保存在原型中的值
但却不能通过对象实例重写原型中的值
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名
那么我们就在实例中创建该属性,该属性会屏蔽原型中的那个值。
使用delete操作符删除实例中创建的同名属性,能够再次访问到原型中的那个值
:::
原型链
:::tip
- 沿着对象的原型prototype向上寻找属性的这个过程会形成一个链条式的继承关系,这个链条叫原型链
- 原型链的顶端是Object.prototype而不是Object(Object也是有原型的)
- 原型的原型一定是由Object构造出来的
::: - 面试题1
function Teacher(){
this.students = 500;
}
var teacher = new Teacher();
function Student(){
}
Student.prototype = teacher;
var student = new Student();
student.students++
// student.students = student.students+1
// 给student重新赋值了属性students
// 而不是修改student原型上的属性
console.log(student,teacher)
// Student {students: 501} Teacher {students: 500}
// student.prototype.students = 100 对象实例没有prototype属性,只能通过__proto__访问原型
students.__proto__.students = 101 //students is not defined
console.log(students)
- 面试题2
function Car(){
this.brand = 'Benz';
}
Car.prototype = {
brand:'Mazda',
intro:function(){
console.log('我是'+ this.brand +'车');
}
}
var car = new Car();
// this 指向的是 car 本身 ,this:谁调用指向谁
car.intro(); //我是Benz车
// 以下两个相等
car.__proto__.intro()
Car.prototype.intro()
// VM882:8 我是Mazda车
- 面试题3
function Person(){
this.smoke = function(){
this.weight--
}
}
Person.prototype = {
weight:130
}
var p = new Person();
p.smoke() //函数执行后返回undefined
// 原因。普通函数都有返回值,默认是undefined。构造函数的返回值是this
console.log(p)//Person {weight: 129, smoke: ƒ}
p.prototype.weight;//130
显式原型和隐式原型
- 每个函数function都有一个prototype属性,即显式原型
- 每个实例都有一个__proto__属性,为隐式原型
- 对象的隐式原型的值为其对应构造函数的显式原型的值
- 总结
- 函数的prototype属性:在定义函数的时候自动添加,默认值是一个空的object
- 实例对象的__proto__属性:在创建对象的时候自动添加的,默认值为构造函数的prototype属性
- 程序员能直接操作显示原型,但不能直接操作隐式原型(es6之前)
//每个函数function都有一个prototype属性,即显式原型
function Fn(){} // 内部语句:this.prototype={}
console.log(Fn.prototype)
//每个实例都有一个__proto__属性,为隐式原型
var fn = new Fn() // 内部语句:this.__proto__ = Fn.prototype
console.log(fn.__proto__)
//对象的隐式原型的值为其对应构造函数的显示原型的值
console.log(Fn.prototype===fn.__proto__) //true
//给原型添加方法
Fn.prototype.test = function(){console.log('test()')}
//通过实例对象来调用原型上的方法
fn.test()
原型链继承
构造函数的实例对象自动拥有函数原型对象的属性(方法)
利用的就是原型链
- 函数的显式原型指向的对象默认是空object实例对象(但是object不满足)
console.log(Fn.prototype instanceof Object) //true console.log(Object.prototype instanceof Object)//false console.log(Function.prototype instanceof Object)//true
- Function是它自身的实例
所有的函数都是Function的实例包括它本身console.log(Function.__proto__ === Function.prototype)//true
- Object 的原型对象是原型链的尽头
console.log(Object.prototype.__proto__)//null
原型链属性问题
function Fn(){}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a)//'xxx'
var fn2 = new Fn()
fn2.a='yyy'
console.log(fn1.a,fn2.a)//'xxx' 'yyy'
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype.setName=function(name){this.name=name}
var p1 = new Person('tom',12)
p1.setName('Bob')
console.log(p1)
- 读取对象的属性时:会自动到原型链中查找
- 设置对象的属性时,不会查找原型链,如果当前对象没有此属性,直接添加此属性并设置其值
- 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
探索instanceof
function Foo(){}
var f1 = new Foo()
console.log(f1 instanceof Foo) //true
console.log(f1 instanceof Object) //true
console.log(f1 instanceof Function) //false
instanceof 是如何判断的
表达式:A instanceof B
如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false
console.log(Object instanceof Function) //true
console.log(Object instanceof Object) //true
console.log(Function instanceof Object) //true
console.log(Function instanceof Function) //true
function Foo(){}
var foo = new Foo()
console.log(Object instanceof Foo) //false
console.log(foo instanceof Foo) //true
console.log(foo instanceof Function) //false
// foo is also an instance of Object because
// Foo.prototype is an instance of Object.
// the interpreter will find the constructor
// through the prototype chain.
// Prototype chain of the object foo
// foo -> Foo.prototype -> Object.prototype -> null
// But foo is not an instance of Function, because
// we could not find Function.prototype in foo's
// prototype chain.
为何Object instanceof Function和Function instanceof Object都返回true?
Object, Function, Array等等这些都被称作是构造“函数”,他们都是函数。而所有的函数都是构造函数Function的实例。从原型链机制的的角度来说,那就是说所有的函数都能通过原型链找到创建他们的Function构造函数的构造原型Function.protorype对象,所以:js Object instanceof Function == true
与此同时,又因为Function.prototype是一个对象,所以他的构造函数是Object. 从原型链机制的的角度来说,那就是说所有的函数都能通过原型链找到创建他们的Object构造函数的构造原型Object.prototype对象,所以:js Function instanceof Object==true
有趣的是根据我们通过原型链机制对instanceof进行的分析,我们不难得出一个结论:Function instanceof Function 依然返回true, 原理是一样的
for(var…in…)
:::tip
-
遍历对象时,自己的属性和原型链上的属性都会被打印出来
-
不建议用它来遍历数组
::: -
遍历对象
//函数执行完 把对象this返回
function Car(){
this.brand = 'Benz'
this.color = 'red'
this.displacement='3.0'
}
Car.prototype = {
lang:5,
width:2.2
}
var car = new Car()
for(var key in car){
console.log(key+':'+car[key])
}
/**
自己的属性和原型链上的属性都会被打印出来
brand:Benz
color:red
displacement:3.0
lang:5
width:2.2
**/
- 遍历数组
var arr = [1,2,3,4]
for(var i in arr){
console.log(i)
}
数组看似正常,假如给数组原型上添加方法,原型链上的值也会被打印出来
Object.prototype.num = 1;
Array.prototype.test = 3
var arr = [1,2,3]
for(var i in arr){
console.log(i,arr[i]);
}
// VM99:2 0 1
// VM99:2 1 2
// VM99:2 2 3
// VM99:2 test 3
// VM99:2 num 1
// undefined
hasOwnProperty、in
:::tip
- hasOwnProperty 用于检查给定的属性是否在当前对象的实例中(而不是在实例的原型中)是否存在
- hasOwnProperty 是排除了原型,in 没有排除原型
- 因此只要in操作符返回true,而hasOwnProperty返回false 就可以确定属性是原型中的属性
:::
function Car(){
this.brand = 'Benz'
this.color = 'red'
this.displacement='3.0'
}
Car.prototype = {
lang:5,
width:2.2
}
var car = new Car()
//hasOwnProperty 只打印自己的属性
for(var key in car){
if(car.hasOwnProperty(key)){
console.log(key+':'+car[key])
}
}
- hasOwnProperty 是排除了原型, in 没有排除原型
//-----------------
function Car(){
this.brand = 'Benz'
this.color = 'red'
}
Car.prototype = {
displacement:'3.0'
}
// 没有通过构造函数,而是对象字面量
var car = {
brand:'Benz',
color:'red'
}
console.log('displacement' in car)//false
// 通过构造函数,
var car = new Car()
console.log('displacement' in car)//true
in 和 for(var…in…)循环的区别
:::tip
- in 参考的是原型链上的所有属性
- for(var…in…) 参考的是原型链上的所有自定义属性
- 在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的(enumerated)属性,
- 其中既包括存在于实例中的属性,也包括存在于原型中的属性。
- 屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回。
自己对于这一点的理解:Object.toString 属性是不可以枚举的,假如我们在对象实例中重新定义了toString属性,这个属性也会遍历出来哦
- 因为根据规定,所有开发人员定义的属性都是可枚举的。
:::
// ……
console.log('toString' in car)//true
for(var i in car){
console.log(i,car[i]);
}
// VM234:2 brand Benz
// VM234:2 color red
// VM234:2 displacement 3.0
// VM234:2 num 1
Object.keys()
- 要取得对象实例上所有可以枚举的实例属性,可以使用ES5中的Object.keys()方法
function Person(){}
Person.prototype.name = 'Tom'
Object.keys(Person.prototype)//["name"]
var p1 = new Person()
p1.aliasName = 'Job'
p1.age = 31
Object.keys(p1)// ["aliasName", "age"]
Object.getOwnPropertyNames()
- 如果你想得到所有实例属性,无论是否可枚举,都可以使用Object.getOwnPropertyNames()方法
Object.getOwnPropertyNames(p1)//["aliasName", "age"]
Object.getOwnPropertyNames(Person.prototype)//["constructor", "name"]
//注意结果中包含了不可枚举的constructor属性
- Object.keys、Object.getOwnPropertyNames 这两个方法都可以用来替代for-in循环
更简单的原型语法
function Person(){}
Person.prototype = {
name:'Tom',
sayName:function(){
alert(this.name)
}
}
在上面的代码中,我们将Person.prototype设置为等于一个对象字面量形式创建的对象。看起来最终结果相同,但有一个例外:
constructor属性不再指向Person了。
前面说过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。
而我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新的对象的constructor属性(指向Object构造函数),不再指向Person构造函数。
此时尽管 instanceof 还能返回正确的结果,但通过constructor已经无法确定对象的类型了。
var f = new Person()
f instanceof Object //true
f instanceof Person //true
f.constructor == Person//false
f.constructor == Object //true
如果constructor的值真的很重要,可以特意设置它的值
function Person(){}
Person.prototype = {
constructor:Person,
name:'Tom',
sayName:function(){
alert(this.name)
}
}
f.constructor == Person // true
注意这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。
默认情况下,原生的constructor属性是不可枚举的。
可以使用
function Person(){}
Person.prototype = {
name:'Tom',
sayName:function(){
alert(this.name)
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
原型的动态性
由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都能够立即从实例上反映出来。
即使先创建了实例后然后修改原型对象
var f = new Person()
Person.prototype.sayHi = function(){
alert('hi')
}
f.sayHi()
但是如果重写了整个原型对象情况就不一样了。
它会切断构造函数与最初原型之间的联系。
实例中的prototype指针指向原型而不是构造函数
function Person(){}
var f = new Person()
Person.prototype = {
constructor:Person,
sayHi:function(){
alert('hi')
}
}//把新的对象地址赋给了原型指针
f.sayHi()
在这个例子中,先创建了Person的一个实例,然后又重写了其原型对象。
然后再调用f.sayHi()时发生了错误
因为f指向的原型中不包含以该名字命名的属性。
重写原型对象切断了现有原型与任何之前已经存在的对象实例直接的联系;它们引用的仍然是最初的原型
原生对象的原型
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。
String.prototype.startsWith = function(text){
return this.indexOf(text) == 0
};
var msg = 'Hello world'
alert(msg.startsWith('He'))//true
不推荐在产品化的程序中修改原生对象的原型。
原生对象的问题
原型模式也不是没有缺点,它最大的问题是由其共享的本质导致的。
function Person(){}
Person.prototype = {
name:'Job',
friends:['Tom','Shell'],
sayHi:function(){
alert('hi')
}
}//把新的对象地址赋给了原型指针
var f1 = new Person()
var f2 = new Person()
f1.friends.push('test')
f1.friends == f2.friends //true
Person.prototype中有一个friends属性,它是一个字符串数组。
然后创建了Person的两个实例,
接着修改了f1.friends引用的数组,因为这个属性数组存在于原型之中,所以f2.friends也是被修改了。
可是,实例一般都是要有属于自己的独有属性的。
而这个问题正是很少单独使用原型模式的原因所在。
组合使用构造函数模式和原型模式
创建自定义类型的最常见方法,就是组合使用构造函数模式和原型模式。
构造函数模式用于定义实例属性。
而原型模式用于定义方法和共享的属性。
结果每个实例都会有自己的一份实例属性的副本,但同时又共享着方法的引用,最大限度地节省了内存。
这种混合模式还支持向构造函数传递参数。
function Person(name){
this.name = name
this.friends = ['Tom','Shell']
}
Person.prototype = {
constructor:Person,
sayHi:function(){
alert('hi')
}
}
var f1 = new Person()
var f2 = new Person()
f1.friends.push('test')
f1.friends == f2.friends //false
f1.sayHi == f2.sayHi
这种混合模式是使用最广泛,认同度最高的一种创建自定义类型的方法。
动态原型模式
其他OO语言经验的开发人员看到独立的构造函数和原型时,可能感到非常困惑。
动态原型模式正式致力解决这个问题的一个方案,它把所有信息都封装在了构造函数中
function Person(name,age){
//属性
this.name = name;
this.age = age;
//方法
//这里只有在sayName方法不存在的情况下才会将它添加到原型中
//这段代码只会在初次调用构造函数时才会执行
//此后,原型已经初始化结束。不需要再修改了
//而且这里对原型的修改能够立即在所有实例中得到反映
//所以这种方法可以说是非常完美了
if(typeof this.sayName != 'function'){
//还可以用instanceof方法检查类型
Person.prototype.sayName = function(){
alert(this.name)
}
}
}
var p = new Person('Tom',24)
p.sayName();
寄生构造函数
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。这个函数看起来又很像是典型的构造函数。
function Person(name,age){
var o = new Object()
o.name = name
o.age = age
o.sayName = function(){
alert(this.name)
}
return o
}
var p = new Person('Tom',19)
p.sayName()
在这个例子中
Person函数创建了一个新的对象并初始化然后返回这个对象。
除了使用new操作符之外,这个模式和工厂模式其实是一模一样的。
有一点需要说明:
首先,返回的对象与构造函数或者与构造函数的原型属性之间没有任何关系。
也就是说构造函数返回的对象与在构造函数外面创建的对象没有什么不同。
为此,不能依赖instanceof操作符来确定对象类型。
建议可以使用其他模式的情况下不要使用这种模式
稳妥构造函数模式
参考:https://blog.csdn.net/maomaolaoshi/article/details/73928094
道格拉斯·克罗克福德发明了JavaScript中稳妥对象(durable objects)这一概念。
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象
稳妥构造函数模式与寄生构造模式类似,但有两点不同
- 新创建对象的实例方法不引用this
- 不使用new操作符来调用构造函数
function Person(name,age,job){
//创建要返回的对象
var o=new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName=function(){
alert(name);
}
//返回对象
return o;
}
//调用
var friend=Person("Nicholas",29,"Software Engineer");
friend.sayName();//"Nicholas"
- 如何实现私有变量和函数?
在实现私有变量和函数之前,我们先来弄懂什么是私有变量。
私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
function Person(name,age,job){
//定义私有(局部)变量
var name=name,
age=age,
job=job;
}
var person1= Person("猫猫老师",18,"前端");
console.log(person1.name)//Cannot read property 'name' of undefined
name、age、job都是这个函数的私有变量,在当前状态下,你没有任何办法访问到这些变量。
知道了这些以后,我们来看看如何实现私有变量和函数。
function Person(name,age,job){
//创建要返回的对象
var o=new Object();
//定义私有(局部)变量
var name=name,
age=age,
job=job;
//添加方法
o.sayName=function(){
alert(name);
}
//返回对象
return o;
}
- 如果把私有变量放到对象o上会怎么样?
function Person(name,age,job){
//创建要返回的对象
var o=new Object();
//把私有变量放到对象o上
o.name=name,
o.age=age,
o.job=job;
//添加方法
o.sayName=function(){
alert(name);
}
//返回对象
return o;
}
这会导致非常严重的安全问题!因为我们可以通过修改或添加对象o的方法,从而篡改或获取到函数的变量!
var person1 = Person("猫猫老师", 18, "前端");
//为函数添加方法
person1.sayAge = function() {
console.log(person1.age);
}
person1.sayAge(); //18
//篡改函数的变量
person1.name = "喵";
console.log(person1.name);//喵
//篡改函数的方法
person1.sayName = function() {
console.log(person1.job);
}
person1.sayName(); //前端
- 为什么o.sayName可以访问到函数的私有变量呢?
在问答这个问题前,我们要先了解一个概念——特权方法。
特权方法
我们把有权访问私有变量和私有函数的公共方法称之为特权方法。
function Person(name,age,job){
//定义私有变量
var name=name,
age=age,
job=job,
//定义特权方法
publicMethod=function(){
return "姓名:"+name+
" 年龄:"+age+
" 工作:"+job
}
return publicMethod;
}
var person1= Person("猫猫老师",18,"前端");
console.log(person1());//姓名:猫猫老师 年龄:18 工作:前端
通过调用特权方法,我们获取了函数的私有变量。
特权方法的实现原理:
这里的特权方法实际上是利用了闭包的原理——利用内部函数可以访问外部函数变量。
更加通俗的来说,就是局部变量可以访问全局变量。
var o="全局变量";
function foo(){
var o2=o;
console.log(o2);
}
foo();//全局变量
对于内部函数来说,外部函数就相当于全局环境,自然可以访问它的变量和函数。
o.sayName对于Person来说就是内部函数,那么自然可以访问Person的私有变量
- 可不可以在函数外部添加方法,从而调用函数的私有变量呢?
答案:不可以。
function Person(name,age,job){
//定义私有变量
var name1=name,
age=age,
job=job;
}
Person.sayAag=function(){
console.log(age);
}
Person.prototype.sayJob=function(){
console.log(job);
}
var person1= Person("猫猫老师",18,"前端");
person1.sayAag()//Cannot set property 'sayAag' of undefined
person1.sayJob()// Cannot read property 'sayJob' of undefined
实际上通过“函数名.方法”来为函数拓展方法的方法,我们称之为静态变量。
function Person(name, age, job) {};
//定义静态属性
Person.staticVar = "静态属性";
console.log(Person.staticVar);//静态属性
//定义静态方法
Person.staticFun=function(){console.log("静态方法");}
Person.staticFun();//静态方法
通过“函数名.方法”定义的变量只能通过函数名.方法名来调用!通过实例是无法访问到这些变量的。
function Person(name, age, job) {};
Person.staticVar = "静态属性";
console.log(new Person().staticVar);//undefined
- 稳妥构造函数模式的意义?
首先从上述的4个问题,我们可以对稳妥构造函数模式有一个总结:
稳妥构造函数模式特点:
- 函数会返回的对象,通过对象的方法我们可以访问或修改内部数据。
- 不可以通过为函数添加方法来访问、修改函数的内部数据。
意义:
以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。
即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的方法访问传入到构造函数中的原始数据。
稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境提供的环境下使用。