引用类型之–Object
对象是javascript的灵魂,一切都可视作“对象”.
创建
1.使用new操作符创建,通过构造器函数.
var obj = new Object(); obj.age = 29; obj.name = 'kyle'; function Person (name, age) { this.name = name; this.age = age; } var p = new Person('kyle', 25);
|
2.使用字面量创建
var obj = { age: 29, name: 'kyle' }
|
3.Object.create()
var obj = Object.create({age: 29, name:'kyle'}); var Animal = { name: 'cat', sayName: function(){ console.log(this.name); } }; var animal = Object.create(Animal); animal.sayName(); console.lof( animal.name );
|
属性访问
1.点访问
var person = { name: 'kyle', age: 25 }; console.log(person.name);
|
2.方括号访问
var a = 'age'; console.log(person[a]); var b = {}; person[b] = 'object'; console.log( person ); Object [object Object]: "object" age: 25 name: "kyle" */
|
ES6新特性: 在object字面量表示中key可以使用方括号里包含变量的方式进行定义
let myKey = 'variableKey'; lety obj = { key1: 'One', key2: 'Two', [myKey]: 'Three' };
|
属性特征
数据属性:
Configurable
: 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,在使用Object.defineProperty()进行新属性的定义时,这个属性默认为false
Enumerable
: 表示能否通过for-in循环返回属性,在使用Object.defineProperty()进行新属性的定义时,默认为false
Writable
: 表示能否修改属性的值,在使用Object.defineProperty()进行新属性的定义时,默认为false
Value
: 包括这个属性的数据值,读取属性值从此处读,写入属性的时候把新值保存在这个位置,默认值为undefined
.
使用点操作和方括号访问属性or添加属性并赋值的时候,configurable
, Enumerable
, Writeable
默认都是true
.
如果要修改属性默认的特征,必须使用Object.defineProperty()方法:
var person = {}; Object.defineProperty(person, "name", { enumerable: true, configurable: false, value: 'kyle' }); console.log(person.name); delete person.name; console.log( person.name ); Object.defineProperty(person, "age", { writable: false, enumerable: false, value: '25' }); person.age = 26; console.log(person.age); for (var key in person) { console.log(key); }
|
我们还可以利用Object.keys()来枚举对象的属性,它会返回一个数组,里面的元素是传入对象所有的可枚举属性的属性名.(key)
var person = { name: 'kyle', age: '25' }; var propertiesArr = Object.keys(person); console.log(propertiesArr);
|
类似的,对于多个属性的创建于定义,我们可以使用Object.defineProperties().
var obj = {}; Object.defineProperties(obj, { 'property1': { value: true, writable: true }, 'property2': { value: 'Hello', writable: false } });
|
2.访问器属性
包含一对儿getter和setter函数。读取属性时会调用getter函数,写入访问器属性时,调用setter函数,对应属性get,set,默认值都是undefine
. 数据双向绑定就是通过这项特性完成的.
function Point2D(x, y){ this.x = x; this.y = y; } Object.defineProperty(Point2D.prototype, 'length', { get: function(){ let {x, y} = this; return Math.sqrt(x * x + y * y); }, set: function(len){ let arc = Math.atan2(this.y, this.x); this.x = len * Math.cos(arc); this.y = len * Math.sin(arc); } }); let p = new Point2D(1, 1); console.log(p.length);
|
另外还有一道最近出现的很频繁的面试题,我们来看看:
var foo = (function(){ var o = { a: 1, b: 2, }; return function(key) { return o[key]; } })(); [分析] 需要得到对象的所有属性,往前一步,便是获取这个对象。 OK,在这个函数里我们相当于要把o给返回出来 而且是通过访问某个key的时候达成! 是不是有感觉了! 没错! 就是定义一个getter,里面返回o就行了! o不是某个特定的constructor构建出来的实例,就是一个普通的Object 那我们定义的对象也很清晰了: 就是Object,我们一般定义在prototype上. */ Object.defineProperty(Object.prototype, 'allKeys', { get: function() { return Object.keys(this); } }); console.log(foo('allKeys'));
|
我们来看看数据双向绑定的效果:
注意
我们知道当实例的身上具有与原型同名的属性时,在访问时会出现屏蔽,但是我们要考虑的一个问题是: 当访问属性的时候,就会访问getter,初始化属性名等同于修改属性时,就会读取setter,而实践证明假如我们并没有给实例上的属性定义getter/setter的话,就会去找原型身上的对应同名属性有没有定义getter/setter,如果有,则调用,如果也没有,返回默认值undefined.
这就是为什么我们把setter和getter定义在了prototype上,依然可以实现效果的原因。
原型
对象的创建
在我们创建一个对象时,我们可以利用构造器函数的方式,在这种方式下,我们会使用new
操作符,在这之前我们声明一个用来当做构造器函数的函数时会经历如下过程:
- 声明一个
function
function
自动获得属性prototype
,它指向原型对象
原型对象
自动获得一个constructor
属性,它指向prototype
所在的函数 (constructor是原型对象唯一自动获得的属性)
在使用new
操作符之后发生了这些事情:
- 创建一个基于
constructor
的实例
- 实例身上具有
__proto__
指针,指向构造函数的prototype
- 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
- 执行构造函数中的代码(为这个对象添加新属性)
- 返回新对象(隐式返回)
prototype
prototype是当创造一个函数时,按照一定规则创造出来的原型对象,这个原型对象会自动获取一个constructor属性,它是指向prototype属性所在的函数的指针。原型对象里的被指定给this的属性会被所有通过此构造函数创造出的实例共享,这样就解决了使用构造函数方式创造对象时造成的重复问题,节约了资源,提高了效率。
原型链
我们发现,任何一个实例化的对象都可以调用toString方法,即使我们并没有给其定义这个方法,这是为什么呢? 因为有原型链的存在,考虑如下代码:
console.log( p.toString == p.__proto__.__proto__.toString);
|
使用一个构造函数创造一个实例对象,在此对象上调用相应的属性和方法时,首先查找它本身有没有,如果没有,则顺着__proto__
这个指针去找它的构造函数的原型上有没有,如果没有,再顺着原型的__proto__
向上去找,也就是说,只要存在__proto__
这个指针,在没有找到对应的属性与方法时,查找不会停下,直到没有__proto__
为止,这样的一种形式可行的结构基础就叫原型链
Ps:在实际运用中不建议直接使用proto,因为它非常慢。
Connect them together
function Person(name){ this.name = name; } Person.prototype.sayName = function(){ console.log('My name is :' + this.name); } var p = new Person("kylewh") p.sayName(); console.dir( rel = [ Person.prototype.constructor == Person, Person.prototype == p.__proto__, p.__proto__.constructor == Person, Person.prototype.__proto__ == Object.prototype, Object.prototype.constructor == Object, Object.prototype.__proto__ == null ] );
|
考虑如下代码:
function People (name){ this.name = name; this.sayName = function(){ console.log('my name is:' + this.name); } } People.prototype.walk = function(){ console.log(this.name + ' is walking'); } var p1 = new People('kyle'); var p2 = new People('vanko');
|
现在我们可以画出它的原型图:
可以发现:一切的尽头是null,javascript即是从无到有的一门语言,真是有诗意的解读。
面向对象编程-OOP
聊完了对象,就不得不提传说中的OOP.
OOP是object oriented programming - 面向对象编程的缩写。 它具有三大特性: 封装、继承、多态。
- 封装: 将客观的事物封装成抽象的类,类一般被指派代表一类具有共同属性的事物,也可能具有相应的一些功能,而这些功能的具体实现是不被暴露出来的,用户只能接触到一些”接口“,这些接口告知用户可以使用什么样的功能,却无法探知里面的内容。类似一个黑盒操作模型。封装后的类可以具有更灵活的组合使用方式以及高复用性,提高了开发的效率。
- 继承: 继承可以让某个类型的对象获得另一个类型的属性与方法,而不需要再次手动编写属于自己的同样的属性/方法。使用现有的类,我们可以对这些方法进行拓展。通过继承创建新的类称为子类或派生类,被继承的类称为基类,(父类,超类),继承的过程,是从一般到特殊的过程。实现继承我们可以通过继承和组合实现,继承概念的实现方式有两类,实现继承和接口继承,实现继承是直接使用父类的属性和方法,而无需额外的编码能力,接口继承仅仅继承了属性和方法的名称,但子类需要去对其提供实现的能力。
- 多态: 对于同一个类,在不同的状态下,可能会做出不同的反应,也就是说在内部结构里进行的操作不同,但是都通过相同的方式予以调用。
面向对象例子
- 创建一个 Car 对象,拥有属性name、color、status;拥有方法run,stop,getStatus
fucntion Car(name, color){ this.name = name; this.color = color; this.status = 'stopped'; if( this.run != 'function') { Car.prototype.run = function(){ this.status = 'running'; console.log('Runing'); } } if( this.stop != 'function') { Car.prototype.stop = function(){ this.status = 'stopped'; console.log('Stopped'); } } if( this.getStatus != 'function') { Car.prototype.getStatus = function(){ console.log(this.status); return this.status; } } }
|
- 创建一个 GoTop 对象,当 new 一个 GotTop 对象则会在页面上创建一个回到顶部的元素,点击页面滚动到顶部。