一、认识对象
1.概述
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型
。
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合
。
var obj = {
foo: 'Hello',
bar: 'World'
};
上面代码中,大括号就定义了一个对象,它被赋值给变量obj
,所以变量obj
就指向一个对象。该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是foo: 'Hello'
,其中foo
是“键名”(成员的名称),字符串Hello
是“键值”(成员的值)。键名与键值之间用冒号分隔。第二个键值对是bar: 'World'
,bar
是键名,World
是键值。两个键值对之间用逗号分隔。
2.键名
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以
。上面的代码也可以写成下面这样。
var obj = {
'foo': 'Hello',
'bar': 'World'
};
如果键名是数值,会被自动转为字符串。
var obj = {
1: 'a',
3.2: 'b',
1e2: true,
1e-2: true,
.234: true,
0xFF: true
};
obj
// Object {
// 1: "a",
// 3.2: "b",
// 100: true,
// 0.01: true,
// 0.234: true,
// 255: true
// }
obj['100'] // true
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
var obj = {
p: function (x) {
return 2 * x;
}
};
obj.p(1) // 2
如果属性的值还是一个对象,就形成了链式引用。
var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
var obj = {
name: 'aaa',
}
属性可以动态创建
,不必在对象声明时就指定。
var obj = {};
obj.foo = 1;
obj.bar = 'name';
3.对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
var o1 = {};
var o2 = o1;
o1 = 1;
o2 // {}
4.表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
{ a: 1 }
JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo
属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo
,指向表达式123
。
为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
{ console.log(123) } // 123
上面的语句是一个代码块,而且只有解释为代码块,才能执行。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错
这种差异在eval
语句(作用是对字符串求值)中反映得最明显。
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
上面代码中,如果没有圆括号,eval
将其理解为一个代码块;加上圆括号以后,就理解成一个对象。
二、对象的操作
1.创建对象
对象的创建方法有很多,包括三种:
- 对象字面量(Object Literal):通过
{}
new Object
+动态添加属性;- new 其他类;
// 方式一
var obj = new Object();
obj.foo = 1;
obj.bar = 'abc';
console.log(obj);
// 方式二
var obj2 = {
foo: 1,
bar: 'abc'
}
console.log(obj2);
// 方式三
var obj3 = new Abc();
function Abc() {
this.foo = 1;
this.bar = 'abc';
}
console.log(obj3);
目前我们主要掌握对象字面量的方式,后续我们学习其他两种方式。
对象的使用过程包括如下操作:
访问
对象的属性;修改
对象的属性;添加
对象的属性;删除
对象的属性;
2.访问对象
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
var obj = {
p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。
var obj = {
foo: 1,
bar: 2
}
var foo = 'bar';
obj.foo; // 1
obj[foo]; //2
方括号运行我们在定义或者操作属性时更加的灵活
var message = '吃饭 树胶';
var obj = {
name: 'abc',
[message]: 'def'
}
console.log(obj[message]);//def
方括号运算符内部还可以使用表达式。
var obj = {
12: 34,
0.7: 56
}
obj[0.7]// 56
obj[10+2]//34
3.添加对象的属性(赋值)
点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。
var obj = {};
obj.foo = 'Hello';
obj['bar'] = 'World';
4.查看所有属性
查看一个对象本身的所有属性,可以使用Object.keys
方法。
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
5.属性的删除:delete 命令
delete
命令用于删除对象的属性,删除成功后返回true
。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
上面代码中,delete
命令删除对象obj
的p
属性。删除后,再读取p
属性就会返回undefined
,而且Object.keys
方法的返回值也不再包括该属性。
注意,删除一个不存在的属性,delete
不报错,而且返回true
。
var obj = {};
delete obj.p // true
上面代码中,对象obj
并没有p
属性,但是delete
命令照样返回true
。因此,不能根据delete
命令的结果,认定某个属性是存在的。
只有一种情况,delete
命令会返回false
,那就是该属性存在,且不得删除。
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false
上面代码之中,对象obj
的p
属性是不能删除的,所以delete
命令返回false
。
另外,需要注意的是,delete
命令只能删除对象本身的属性,无法删除继承的属性。
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
上面代码中,toString
是对象obj
继承的属性,虽然delete
命令返回true
,但该属性并没有被删除,依然存在。这个例子还说明,即使delete
返回true
,该属性依然可能读取到值。
6.属性是否存在:in 运算符
in
运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true
,否则返回false
。它的左边是一个字符串,表示属性名,右边是一个对象。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
in
运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj
本身并没有toString
属性,但是in
运算符会返回true
,因为这个属性是继承的。
这时,可以使用对象的hasOwnProperty
方法判断一下,是否为对象自身的属性。
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
7.对象的遍历
对象的遍历(迭代):表示获取对象中所有的属性和方法。
Object.keys()
方法会返回一个由一个给定对象的自身可枚举
属性组成的数组
;
遍历方式一:普通for循环
var infoKeys = Object.keys(info)
for (var i = 0; i < infoKeys.length; i++) {
var key = infoKeys[i]
var value = info[key]
console.log(`key: ${key}, value: ${value}`)
}
遍历方式二:for in 遍历方法
for (var key in info) {
var value = info[key]
console.log(`key: ${key}, value: ${value}`)
}
// 对象不支持: for..of..: 默认是不能遍历对象
// for (var foo of info) {
// }
for...in
循环有两个使用注意点。
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
- 它不仅遍历对象自身的属性,还遍历继承的属性。
如果继承的属性是可遍历的,那么就会被for...in
循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for...in
的时候,应该结合使用hasOwnProperty
方法,在循环内部判断一下,某个属性是否为对象自身的属性。
var person = { name: '老张' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
三、对象的数据结构
1.栈内存和堆内存
我们知道程序是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存和堆内存。
原始类型
占据的空间是在栈内存
中分配的;对象类型
占据的空间是在堆内存
中分配的;
2.值类型和引用类型
原始类型
的保存方式:在变量中保存的是值本身
- 所以原始类型也被称之为
值类型
;
对象类型
的保存方式:在变量中保存的是对象的“引用”
- 所以对象类型也被称之为
引用类型
;
3.对象的一些现象
现象一: 两个对象的比较
var obj1 = {}
var obj2 = {}
console.log(obj1 === obj2)
现象二: 引用的赋值
var info = {
name: "why",
friend: {
name: "kobe"
}
}
var friend = info.friend
friend.name = "james"
console.log(info.friend.name) // james
现象三: 值传递
function foo(a) {
a = 200
}
var num = 100
foo(num)
console.log(num) // 100
现象四: 引用传递, 但是在函数中创建了一个新对象, 没有对传入对象进行修改
function foo(a) {
a = {
name: "why"
}
}
var obj = {
name: "obj"
}
foo(obj)
console.log(obj)
现象五: 引用传递, 但是对传入的对象进行修改
function foo(a) {
a.name = "why"
}
var obj = {
name: "obj"
}
foo(obj)
console.log(obj)
四、this关键字
1.为什么需要this?
在常见的编程语言中,几乎都有this
这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象 语言中的this不太一样:
- 常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在
类的方法
中。 - 也就是你需要有一个类,类中的方法(特别是实例方法)中,this代表的是
当前调用对象
;
但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义;
我们来看一下编写一个obj的对象,有this和没有this的区别:
//没有this
var obj = {
name: "code!mq",
eating: function () {
console.log(obj.name + "正在吃饭");
},
sleep: function () {
console.log(obj.name + "正在睡觉");
}
}
obj.eating();
// 有this
var obj = {
name: "code!mq",
eating: function () {
console.log(this.name + "正在吃饭");
},
sleep: function () {
console.log(this.name + "正在睡觉");
}
}
obj.eating();
2.this指向什么?
函数中是有一个this的变量, this变量在大多数情况下会指向一个对象
情况一: 如果普通的函数被默认调用, 那么this指向的就是window
function sayHello(name) {
console.log(this);
}
sayHello('code!mq');
情况二: 如果函数它是被某一个对象来引用并且调用它, 那么this会指向这个对象(调用的那个调用)
var obj = {
name: "code!mq",
running: function() {
console.log(this)
// console.log(obj)
// console.log(this === obj)
}
}
obj.running()
后续我们还会学习其他,也会给大家总结this的规律,详见高级篇;
五、类和对象
1.类和对象的思维方式
我们来思考一个问题:如果需要在开发中创建一系列的相似的对象,我们应该如何操作呢?
比如下面的例子:
- 游戏中创建一系列的英雄(英雄具备的特性是相似的,比如都有名字、技能、价格,但是具体的值又不相同)
- 学生系统中创建一系列的学生(学生都有学号、姓名、年龄等,但是具体的值又不相同)
当然,一种办法是我们创建一系列的对象:
var p1 = {
name: "zhangsan",
age: 18,
height: 1.88
}
var p2 = {
name: "lisi",
age: 19,
height: 1.85
}
var p3 = {
name: "wangwu",
age: 29,
height: 1.65
}
这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;
- 我们是否有可以批量创建对象,但是又让它们的属性不一样呢?
2.创建对象的方案 – 工厂函数
我们可以想到的一种创建对象的方式:工厂函数
- 我们可以封装一个函数,这个函数用于帮助我们创建一个对象,我们只需要
重复调用这个函数
即可; - 工厂模式其实是一种常见的设计模式;
// 工厂函数(工厂生产student对象) -> 一种设计模式
// 通过工厂设计模式, 自己来定义了一个这样的函数
function createStudent(name, age, height) {
var stu = {}
stu.name = name
stu.age = age
stu.height = height
stu.running = function() {
console.log("running~")
}
return stu
}
var stu1 = createStudent("why", 18, 1.88)
var stu2 = createStudent("kobe", 30, 1.98)
var stu3 = createStudent("james", 25, 2.05)
console.log(stu1)
console.log(stu2)
console.log(stu3)
3.认识构造函数
工厂方法
创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型
- 但是从某些角度来说,这些对象应该有一个他们
共同的类型
; - 下面我们来看一下另外一种模式:构造函数的方式;
我们先理解什么是构造函数?
- 构造函数也称之为
构造器
(constructor),通常是我们在创建对象时会调用的函数; - 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
- 但是JavaScript中的构造函数有点不太一样,构造函数扮演了其他语言中类的角色;
也就是在JavaScript中,构造函数
其实就是类的扮演者:
- 比如系统默认给我们提供的
Date
就是一个构造函数,也可以看成是一个类; - 在ES5之前,我们都是通过
function
来声明一个构造函数(类)的,之后通过new关键字来对其进行调用; - 在ES6之后,JavaScript可以像别的语言一样,通过
class
来声明一个类;
// JavaScript已经默认提供给了我们可以更加符合JavaScript思维方式(面向对象的思维方式)的一种创建对象的规则
// 在函数中的this一般指向某一个对象
/*
如果一个函数被new操作符调用
1.创建出来一个新的空对象
2.让this指向这个空对象
3.执行函数体的代码块
4.如果没有明确的返回一个非空对象, 那么this指向的对象会自动返回
*/
function coder(name, age, height) {
this.name = name
this.age = age
this.height = height
this.running = function() {
console.log("running~")
}
}
// 在函数调用的前面加 new 关键字(操作符)
var stu1 = new coder("why", 18, 1.88)
var stu2 = new coder("kobe", 30, 1.98)
console.log(stu1, stu2)
那么类和对象到底是什么关系呢?
2.类和对象的关系
那么什么是类(构造函数)呢?
- 现实生活中往往是根据一份描述/一个模板来创建一个实体对象的.
- 编程语言也是一样, 也必须先有一份描述, 在这份描述中说明将来创建出来的对象有哪些属性(成员变量)和行为(成员方法)
比如现实生活中,我们会如此来描述一些事物:
- 比如水果fruits是一类事物的统称,苹果、橘子、葡萄等是具体的对象;
- 比如人person是一类事物的统称,而Jim、Lucy、Lily、李雷、韩梅梅是具体的对象;
3.JavaScript中的类(ES5)
我们前面说过,在JavaScript中类的表示形式就是构造函数
。
JavaScript中的构造函数是怎么样的?
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
- 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;
如果一个函数被使用new操作符调用了,那么它会执行如下操作:
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
- 构造函数内部的this,会指向创建出来的新对象;
- 执行函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象;
接下来,我们可以用构造函数的方式来实现一下批量创建学生。
4.创建对象的方案 – 构造函数(类)
我们来通过构造函数实现一下:
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
this.eatting = function () {
console.log(this.name + '吃饭饭');
}
}
var p1 = new Person('zhangsan', 12, 'gd');
var p2 = new Person('lis', 16, 'sh');
这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性,这个我们后续再探讨);
事实上构造函数还有很多其他的特性:
- 比如原型、原型链、实现继承的方案
- 比如ES6中类、继承的实现;
5.额外补充-全局对象window
浏览器中存在一个全局对象object -> window
作用一: 查找变量时, 最终会找到window头上
作用二: 将一些浏览器全局提供给我们的变量/函数/对象, 放在window对象上面
作用三: 使用var定义的变量会被默认添加到window上面
console.log(window)
// 使用var定义变量
var message = "Hello World"
function foo() {
// 自己的作用域
// abc()
// alert("Hello World")
console.log(window.console === console)
// 创建一个对象
// var obj = new Object()
console.log(window.Object === Object)
// DOM
console.log(document)
// window.message
console.log(window.message)
}
foo()
6.额外补充-函数也是对象
// 定义原始类型的变量
var name = "why"
var age = 18
// 定义对象类型的变量
// 地址 - 指针 - 引用
var obj = {} // 堆内存
var foo = function() {} // 堆内存
function bar() {} // 堆内存
console.log(typeof obj) // object
console.log(typeof foo) // function -> object
// var stu = new Student() // stu是一个Student -> Person
// 引申一些别的知识(了解)
var info = {}
info.name = "abc"
function sayHello() {
}
sayHello.age = 18
console.log(sayHello.age)
function Dog() {
}
// 构造函数上(类上面)添加的函数, 称之为类方法
Dog.running = function() {}
Dog.running()