JavaScript之对象和原型
上篇回顾:
- 什么是作用域? 两种定义: (1)负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限;(2)执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
- 词法作用域(两种欺骗作用域);
- 作用域提升;
- 块级作用域;
- 作用域链:全局环境存在全局执行环境;函数具有自己的执行环境。当代码在一个执行环境中执行时,会创建一个变量的作用域链,来保证执行环境对所有变量和函数的有序访问。作用域的前端,始终是当前执行环境的变量对象,作用域的后端,始终是全局变量对象。
- 闭包:简单来说,闭包是指有权访问另一个函数作用域中的变量的函数。满足两个条件:1.是一个函数;2.能够访问另一个函数作用域中的变量。
- 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。
- 循环闭包问题
- 闭包会引起一些问题:this指向改变;内存泄漏问题。
一、对象
对象是JavaScript
中非常重要的数据结构。
1.基础回顾
JavaScript基本数据类型:string、number、null、undefined、boolean (ES6中新增了Symbol类型) 补充知识点: ES6 为什么引入了Symbol?
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 作为属性名,每一个Symbol值都是不相等的。
// 无参数
let a = Symbol();
let b = Symbol();
a === b //false
// 有参数
let c = Symbol('foo');
let d = Symbol('foo');
c === c //false
复制代码
注意:
- Symbol 值作为对象属性名时,不能用点运算符。
- 在对象内部,使用Symbol定义属性时,Symbol值必须放在方括号[]内。
复杂数据类型:Object * JavaScript
的对象是一种无序的集合数据类型,它由若干键值对组成。 * 一个JavaScript
对象可以有很多属性,属性定义了对象的特征。 * 访问属性是通过.操作符完成的,但这要求属性名必须是一个有效的变量名;对象的属性也可以通过方括号访问或者设置。
一个属性的名称如果不是一个有效的
JavaScript
标识符(例如,一个由空格或连字符,或者以数字开头的属性名),就只能通过方括号标记访问。
var myObject = {
'3': 2
};
myObject.'3'; // Uncaught SyntaxError: Unexpected string
myObject['3']; // 2
复制代码
2.对象的内容
对象的内容其实就是对象的属性,至于对象的属性到底是怎样存储的,实际上存储在对象容器内部的是这些属性的名称,他们就像指针一样指向属性值存储的真正的位置(在后面的JavaScript内存机制中会介绍)。在 ES5 之前,JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。但是从 ES5 开始,所有的属性都具备了属性描述符。 属性描述符分为数据描述符和访问(存取)描述符。
- 数据描述符 通过getOwnPropertyDescriptor方法可以获取对象的数据描述符。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
复制代码
在创建普通对象的时候,数据描述符使用的是默认值(和上面输出的一样),但是我们根据自己的需要,可以使用Object.defineProperty()来添加一个新属性或者修改某个属性。 value:属性的值。 writable:是否可写。当设置为false的时候,表明这个属性的value是不能被改变的(注意:严格模式下,如果writable为false,对属性进行再次赋值,会报typeError
的错误!)。 enumerable:是否可枚举。我们有的时候需要对某个对象的属性进行遍历,如果enumerable为false,则改属性不会被遍历,也就不会出现在枚举中。 configurable: 只有当属性的configurable为true时,才能通过Object.defineProperty()来改变数据描述符,否则会报typeError
错误。而且configurable的属性不能由false改为true。 例外: 修改value的值不受configurable值的影响;当configurable为false时,writable只能由true改为false,不能由false改为true。
delete可以删除对象的属性,但是configurable必须为true。
- 访问描述符(getter/setter) 读取属性使用getter,负责返回有效的值;写入属性使用setter,负责处理数据。对于访问描述来说,JavaScript会忽略他们的value和writable属性,而是关心set、get、enumerable、configurable属性。 如何访问一个属性? 访问属性时,引擎实际上会调用内部的默认 [[Get]] 操作(在设置属性值时是 [[Put]]),[[Get]] 操作会检查对象本身是否包含这个属性,如果找到就会返回这个属性的值,如果没找到的话还会查找 [[Prototype]]链。
3.枚举一个对象的所有属性
for...in
循环:该方法依次访问一个对象及其原型链中所有可枚举的属性(不含Symbol
属性)。Object.keys(obj)
:该方法返回一个对象 obj 自身包含(不包括原型中)的所有属性的名称的数组。Object.getOwnPropertyNames(obj)
:该方法返回一个数组,它包含了对象obj
所有拥有的属性(无论是否可枚举)的名称。Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身的所有Symbol
属性的键名。Reflect.ownKeys(obj)
:Reflect.ownKeys
返回一个数组,包含对象自身的所有键名,不管键名是Symbol
或字符串,也不管是否可枚举。
4.创建对象(主要介绍前5种)
- 对象字面量
let person = {
name: 'Luna',
age: '18',
hobby: 'reading',
greeting: function() {
console.log('hello,I am ' + this.name)
}
};
复制代码
- 使用
new
表达式
let person = new Object();
person.name = 'Luna';
person.age = '18';
person.hobby = 'reading';
person.greeting = function () {
console.log('hello,I am ' + this.name)
}
复制代码
使用这两种方式的好处是简单,但是缺点是如果使用同一个接口创建很多对象,会产生大量的重复代码。
- 使用工厂模式
函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建 多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
function ceatePerson(name, age, hobby) {
let obj = new Object();
obj.name = name;
obj.age = age;
obj.hobby = hobby;
this.greeting = function () {
console.log('hello,I am ' + this.name)
}
return obj;
}
let person = cratePerson('Luna','18','reading');
复制代码
- 使用构造函数
使用构造函数有几个缺点:1.没有显示的创建这个对象;2.直接将属性和方法赋给了this
对象;3.没有return
语句。
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.greeting = function () {
console.log('hello,I am ' + this.name)
}
}
let person = new Person('Luna','18','reading');
复制代码
补充知识点:
什么是构造函数?
构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的唯一区别在于调用方式的不同,使用 new 生成实例的函数就是构造函数,直接调用的就是普通函数。constructor
返回创建实例对象时构造函数的引用。
构造函数的缺点:每个方法都要在每个实例上重新创建一遍。
- 原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那 么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是 可以将这些信息直接添加到原型对象中,
function Person () {}
Person.prototype.name = 'Luna';
Person.prototype.age = '18';
Person.prototype.hobby = 'reading';
Person.prototype.greeting = function () {
console.log('hello,I am ' + this.name)
}
let person = new Person()
复制代码
- 组合使用构造函数模式和原型模式
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function () {
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
复制代码
- 动态原型模式
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
复制代码
- 寄生构造函数模式
function Person (name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
复制代码
- 稳妥构造函数模式
function SpecialArray () {
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
//返回数组
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
复制代码
5.对象的方法
- Object.is():ES6 提出“Same-value equality”(同值相等)算法(ES5:JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。),用Object.is()解决严格相等的问题。
与"==="的区别:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
复制代码
- Object.assign():通过复制一个或多个对象来创建一个新的对象(属于浅拷贝【Object.assign()浅拷贝在后面章节会进行讲解】)。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
复制代码
- Object.create():会创建一个对象并把这个对象的 [[Prototype]] 关联到指定的对象。
var person = {
name: 'Luna',
age: '18',
hobby: 'reading',
greeting: function() {
console.log('hello,I am ' + this.name)
}
};
var me = Object.create(person);
me.name = 'Bella';
复制代码
二、原型
1.基础回顾
JavaScript也是面向对象的,而面向对象的一个重要的方法就是继承。A对象通过继承B对象,就可以拥有B对象的所有属性和方法。我们知道,java是通过类来是实现的,但是对于JavaScript来说,在ES6以前,是通过原型来实现的(ES6提出了class)。
构造函数、原型和实例的关系: 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
-
prototype
只有函数拥有这个属性,指向一个对象。对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。原型对象的作用,就是定义所有实例对象共享的属性和方法。 -
__proto__
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。当我们使用new操作符时,生成的实例就有了__proto__
属性。
let fun = Function.prototype.bind()
复制代码
function Person(name, hobby) {
this.name = name;
this.hobby = hobby;
this.greeting = function () {
console.log('hello,I am ' + this.name)
}
}
let person = new Person('Luna','reading');
Person.prototype; // constructor
Person.prototype.constructor; // Person()
person.__proto__;// constructor
Person.prototype.age = 18;
person.name; // Luna
person.age; // 18
复制代码
2.原型链
每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。
当我们访问一个对象的属性时,引擎会先调用内置的[GET]方法,检查对象本身是否有该属性;如果没有,再找他的原型对象,如果没有找到,再一层议程往上找他的原型,原型的尽头是null。 在这张原型链图中,有几点需要注意:Object()也属于构造函数,所以他的原型对象是Function.prototype.
3.经典面试题之手写一个new实现
从上面的例子中,我们可以看出,new创建的实例即可以访问到其构造函数里的属性,也可以访问到原型里的属性。
当代码 new Foo(...) 执行时,会发生以下事情:
一个继承自 Foo.prototype 的新对象被创建。
使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
function create() {
// 创建一个空的对象
var obj = new Object(),
// 获得构造函数,arguments中去除第一个参数
Con = [].shift.call(arguments);
// 链接到原型,obj 可以访问到构造函数原型中的属性
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
// var ret = Con.apply(obj, arguments);
var ret = Object.create(obj);
// 优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
复制代码
我们来测试一下:
// 测试用例
function Car(color) {
this.color = color;
}
Car.prototype.start = function() {
console.log(this.color + " car start");
}
var car = create(Car, "black");
car.color;
// black
car.start();
// black car start
复制代码
三、总结
- 对象是7个基础类型之一;
- 对象是键值对的集合,可以通过.和[]获取属性值;
- 属性的特性可以通过属性描述符来控制;
- 属性不一定包含值,可能是具备 getter/setter 的“访问描述符”;
- 可以使用for in遍历(一共有五种方式)属性名,使用for of遍历属性的值;
- 创建对象的几种常见方法:字面对象;new表达式;工厂模式;构造函数模式;原型模式;
- 每个对象实例都对应一个原型对象,当我们访问一个对象的属性时,会先触发内置的[GET]方法,找该对象是否有此属性,如果没有,会找他的原型对象,原型对象没有,再沿着原型链一层一层往上找,知道原型链的尽头--null,最后没有找到,会返回undefined;
- 如何手写一个new实现。
参考:
ECMAScript 6 入门 --阮一峰
Object|MDN 深度解析new原理及模拟实现
《你不知道的JavaScript--上》
《JavaScript高级程序设计--第三版》