对象
先了解一下“原始值”与“引用值”:
- 引用值包括数组、函数、对象等。
- 原始值存放在栈内存,引用值存放在堆内存。
- 原始值没有属性和方法,引用值有属性和方法。
- 有些原始值有包装类,所以即使表示成原始值,也可以在调用其属性和方法时不报错,但其中有需要注意的问题(下面讲解)。
对象的创建
(1)对象字面量/对象直接量(PlainObjejct)
- 语法:var obj = {…}
- 实例:
<script type="text/javascript">
var student = {
name: "zjy",
id: "1001",
eat : function() { //普通的函数...可以有参数,也可以有返回值
document.write('eateateateat...');
}
}
//delete student.name; //删除属性
//student.name = "zyy"; //修改属性
//student.sex = "女"; //增加属性
//obj.f = function () { ... }//增加方法
</script>
- 控制台中查看对象:
- 控制台中操作对象:
(注意上面这幅图:变量在未定义时试图打印,将报错;但一个对象的属性未定义却试图打印时,结果为undefined!)
(2) 使用构造函数创建对象
(js中的构造函数相当于java中的类)
系统自带的构造函数
var obj = new Object();
var str = new String();
var num = new Number();
//后续也可为这些对象增加属性或方法
自定义构造函数
- 实例:
<script type="text/javascript">
//自定义构造函数与普通函数在书写形式上没有任何区别;
//我们人为规定,构造函数的函数名的首字母大写.
function Car(color) {
this.color = color; //this.color 由参数决定
this.name = "BMW"; //this.name 被写死了,但后续可进行修改
this.height = "1400";
this.lang = "4900";
this.weight = 1000;
this.run = function () {
console.log('runrunrunrun...');
}
}
var car = new Car();
var car1 = new Car('black');
car.name = "dazhong";
</script>
<script type="text/javascript">
function Student(id, name, age) {
this.id = id;
this.name = name;
this.age = age;
}
</script>
- 根据上面的实例,思考构造函数的执行过程?
- 执行步骤如下所示:
function Car(color) {
// Step1、在函数体最前面隐式的加上this = {}; 表示声明一个空对象(事实上,不是一个空对象,其内是有东西的,7.3.5节将讲解到):
// var this = { };
// Step2、执行this.xxx = xxx,使得属性具有对应的值:
// color : "",
// name: "",
// ...
this.color = color;
this.name = "BMW";
this.height = "1400";
this.lang = "4900";
this.weight = 1000;
// Step3、隐式的返回this:(s(即使捣乱性的自己随便return一个,最终系统也会强制return this))
// return this;
}
- 下面根据上面构造函数执行的过程,来自己写一个“更具内涵的”构造函数(当然,实际不需写的太有内涵,而直接写成上上面那样…)
function Person(name, height) {
var that = {};
that.name = name;
that.height = height;
return that;
}
var person = Person();
var person1 = Person('lily', 180);
仍可以在控制台中查看到:
原始值与对象(包装类)
- 原始值:指直接var出来的变量值…如,var str = ‘abcd’
- 对象:使用new创建出来的对象…如,var str = new String(‘abcd’)
对象里有属性和方法,而原始值只是作为个体存在,没有属性和方法,这在一定程度上造成了不便。于是产生了包装类。所谓包装类,是指把原始值包装成引用值…如下——分析下面的例子:原始值没有属性,但为何下面的程序没有报错,而输出“undefined”?
var num = 4;
num.length = 3;
console.log(num.length);
解释:上述代码实际上内部进行了隐式的包装类转换——原始值调用属性就会导致隐式转换成包装类。
var num = 4;
num.length = 3; //这里原始值调用属性,因此导致隐式转换成包装类:new Number(4).length = 3; 这样做后就创建了这样一个num.length变量,但创建完后就立即销毁这个new对象(因为留着的话系统不知道有啥用)
console.log(num.length);//这里的“num.length”又会导致隐式转换成包装类::new Number(4).length;并没有对其赋值,且与上面的“new Number(4).length = 3; ”无关(因为它已经被销毁了)。因此最终输出undefined
例题:
var str = "abcd";
str.length = 3; //new String('abcd').length = 3; 创建完后就删除
console.log(str);//结果:abcd
console.log(str.length);//结果:4。自动创建new String('abcd')并访问字符串本来就有的length属性(用来计算字符串长度),计算得4。
var arr = [0, 1, 2, 3];
arr.length = 2;
console.log(arr); //结果:0, 1
console.log(arr.length);//结果:2
//注意,这里arr数组本身就是对象,所以有属性,且具有截断操作的方法。本来arr.length为4,但我们的语句arr.length = 2;就使得数组的长度设置为2,即进行了截断操作。
var str = "abc";
str += 1;
var test = typeof(str); //使得:test = "String"
if(test.length == 6) { //原始值调用属性,隐式转换成包装类new String("String");长度确实为6,所以进入if语句
test.sign = "..."; //test为原始值,所以会发生包装类的隐式转换:new String(test).sign = '...';执行完就销毁
}
document.write(test.sign);//这里又发生一次新的没有赋值的类型转换:new String(test).sign;最后打印结果为undefined,不是“...”。
//下面是个预编译考题,问:xyz最终分别是什么?
var x = 1, y = z = 0;
function add(n) {
return n = n + 1;
}
y = add(x);
function add(n) {
return n = n + 3;
}
z = add(x);
//我的错误答案:1 2 4
//正确答案:1 4 4
//因为add(x)执行的都是第二个函数!!因为函数整体提升!先出现的函数被后面出现的同名的函数覆盖。。
原型
(1)定义
原型的实质是个对象,这个对象通过构造函数的prototype
属性获得(注意:必须是构造函数名来调用,而不能使用利用构造函数创建出来的对象来调用prototype
属性)。调用该属性,即获得这个构造函数的父类——称为“原型”。为这个“父类”(时刻注意它的实质也是个对象哦)添加一些属性或方法,那么该构造函数产生的对象将都可以继承这个"父类"的属性或方法(应参照下面的例子理解这句话)。
(PS. 一般的构造函数如果没有给其指定原型,那么其原型就是Object类。且所有构造函数的最高父类都是Object类。)
<script type="text/javascript">
function Person() {
}
Person.prototype.sex = '女'; //在Person的原型上的创建了sex属性
var person1 = new Person();
var person2 = new Person();
</script>
构造函数中并没有sex属性,实际上是从其父类Person.prototype上找到的sex属性。
- 验证“原型的实质是个对象”:
从上面看出:原型中默认含有constructor函数,这个函数是构造函数,即, constructor相当于一个构造器:
> 这个constructor可以手动更改:<script type="text/javascript"> function Person() { } Person.prototype.sex = '女'; function Car() { } Person.prototype.constructor = Car; </script>
(2)特点
- 如果构造函数(时刻要注意,这里的“构造函数”相当于JAVA中的“类”)中有与原型相同的属性,则创造出来的对象的该属性的值与构造函数中的值一致,而不是与原型中的一致。(用已学过的Java中的知识解释:子类“重写”了父类的属性或方法,最后新建子类对象时,其值肯定与子类的一致…)
(3)应用
原型的实际应用是:将构造函数的对象的公共属性添加到原型上,而不再写于构造函数内。这样每次新建一个对象时,就可以使计算机少做一些工作了。
<script type="text/javascript">
function Person(name, ID_Card) {
this.name = name;
this.ID_Card = ID_Card;
}
// 利用原型创建公共属性的方式1:
Person.prototype.country = 'China'; //“用此构造函数创建出来的对象——人,都来自China”
Person.prototype.eat = function() {
console.log("eateateat...");
}
// 利用原型创建公共属性的方式2:
// Person.prototype = {
// country : China;
// eat : function() {
// console.log("eateateat...");
// }
// }
var person1 = new Person("zjy", 1001);
var person2 = new Person("zyy", 1002);
</script>
(4)原型中属性或方法的增删改查操作
- 改:通过代码或通过控制台重新为其属性赋值
<script type="text/javascript">
function Person(name, ID_Card) {
this.name = name;
this.ID_Card = ID_Card;
}
Person.prototype.country = 'China';
Person.prototype.country = 'US';//修改属性
var person1 = new Person("zjy", 1001);
var person2 = new Person("zyy", 1002);
</script>
- 增:通过代码或通过控制台添加新的属性或方法
- 删:通过代码或通过控制台删除属性或方法
(5)__ proto __
如何找到一个对象的原型呢?通过对象的隐式属性__ proto __来查看/找到原型,此时,就可以使用构造函数构造出来的对象找到原形了,而不是使用构造函数来找到原型。
<script type = "text/javascript">
Person.prototype = {
name : 'a',
sayName : function() {
console.log(this.name);
}
}
function Person() {
this.name = "b";
}
var person = new Person();
</script>
【“__XXX __”命名方式的变量表示系统命名的隐式属性。另外,在写构造函数时,如果想时某个变量是“私有”的,则这样命名:“_XXX”(与Python类似)。】
NO1、
<script type = "text/javascript"> function Person() { } var person = new Person(); </script>
- 可以看出,__ proto __里含有原型prototype。
NO2、
<script type = "text/javascript"> Person.prototype.name = 'abc'; function Person() { } var person = new Person(); </script>
《7.1.2.1 自定义构造函数》中讲解了构造函数的执行过程,说到“第一步是在构造函数内部首先隐式的加上var this={},表示声明一个空对象”,但其实并不是空的:
<script type = "text/javascript">
Person.prototype.name = 'abc';
function Person() {
// var this = {
// __proto__ : Person.prototype
// };
}
var person = new Person();
</script>
当访问对象中的属性时,如果发现没有自定义该属性,则通过__proto__指向的索引去找Person.prototype上有无该属性,即寻找系统定义的属性。因此,__proto__起一个连接的作用——把自定义属性与系统属性连接起来了。如,通过下面的方式访问name属性:
- 可以通过下面的方式改变__proto __的值:
方式1、
<script type = "text/javascript"> Person.prototype.name= 'abc'; function Person() { } var obj = { name : "sunny" } var person = new Person(); </script>
【注意:person.__proto__ = obj
不可写成Person.__proto__ = obj
,否则修改无效。】
方式2、
在代码中添加
person.__proto__ = obj;
(也不能写成Person.__proto__ = obj;
)。
方式3、
<script type = "text/javascript"> Person.prototype.name = 'abc'; function Person() { } var person = new Person(); Person.prototype.name = 'sunny'; </script>
请注意下面的代码段:
<script type = "text/javascript"> Person.prototype.name = 'abc'; function Person() { } var person = new Person(); Person.prototype = { //修改Person.prototype,令其指向一个新对象(原来的Person.prototype指向“祖先”对象) name : 'sunny' } </script>
- 结果:未能使name属性改变(仍是‘abc’)。
- 对结果的解释:这里修改的是Person.prototype对象,而另一种修改修改的是Person.prototype对象里的name属性,这相当于:
<script type = "text/javascript"> var obj = {name : 'abc'}; var obj1 = obj; obj = {name : 'sunny'}; </script>
最终并未使obj1的name属性改变(但obj的name属性已改变)。对于Person.prototype的修改:
因此,可以看成:<script type = "text/javascript"> Person.prototype = {name : 'abc'}; __proto__ = Person.prototype; Person.prototype = {name : 'sunny'}; </script>
并未使__proto__改变。
再看下面这段代码:
<script type = "text/javascript"> Person.prototype.name = 'abc'; function Person() { } Person.prototype = { //修改Person.prototype,令其指向一个新对象(原来的Person.prototype指向“祖先”对象) name : 'sunny' } var person = new Person(); </script>
结果会使name属性变成‘sunny’,因为new之前已经修改了Person.prototype,所以对应的其中的name属性随之会变化(由‘abc’变为‘sunny’)。
原型链
(1)原型链的构成
原型链的构成相当于本对象的原型指向另一个对象,另一个对象的原型又指向另另个对象…最终,都是指向Object的原型:
如下代码,就构成了一个原型链:
<script type = "text/javascript">
Grand.prototype.lastName = "ggg";
function Grand() {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.fff = 'fff';
this.num = 10;
this.fortune = {
card1 : 'china bank'
}
}
var father = new Father();
Son.prototype = father;
function Son() {
this.sss = 'sss';
}
var son = new Son();
</script>
这个原型链是这样体现的:Son.prototype → Father. prototype → Grand.prototype → Object.prototype
。如下示例,我们可以使用Son对象访问处于Grand.prototype中的lastName属性:
(2)原型链上属性的增删改查
可以查看“上辈儿”原型中的属性,但不能对其删除、修改、增加(只能自己对自己进行这样的操作)。有一特例:如果“上辈儿”原型中的某个属性是引用值,则可以对这一引用值进行修改、增加一类的操作。
但是,如果此时查看father.fortune
,结果却是:
与之类似的,这样一个例子:
(PS. 图中son.num ++
操作后的10
为返回值…不表示结果)
也就是说,如果“修改”“上辈儿”原型中的属性,并没有真正修改…
下面看一个题目:
<script type = "text/javascript"> Person.prototype = { name : 'a', sayName : function() { console.log(this.name); } } function Person() { this.name = "b"; } var person = new Person(); </script>
没有
this.name = "b";
语句时:
有this.name = "b";
语句时:
这是因为this:this指代的是调用sayName()的对象,分别是person与Person.prototype,两对象中有不同的name值,所以有不同的输出。
下面再看一个题目:
<script type = "text/javascript"> Person.prototype = { num : 100 } function Person() { this.count = function () { this.num ++; //return undefined; } } var person = new Person(); </script>
注意:
var obj = {...};
(如var obj = ‘abc’;
)与var obj = new Object();
的obj都是有原型的,推荐使用前者.
(3)使用Object.create()创建对象
Object类中有create()函数,该函数有一个参数——引用对象或原型或null(不能是原始值);该函数的作用是用来创建对象。
- 参数为对象:
<script type = "text/javascript">
var obj = {name : 'sunny', age : 12};
var obj1 = Object.create(obj); //参数为对象
</script>
- 参数为原型:
<script type = "text/javascript">
Person.prototype = {
num : 100
}
function Person() {
}
var person = Object.create(Person.prototype);
// 参数为原型
// 等价于var person = new Person();
</script>
- 参数为null:
<script type = "text/javascript">
var obj = Object.create(null); //参数为null
</script>
call()
作用:改变this指向
解释:
<script type = "text/javascript">
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('zjy', 100);
var obj = {
}
Person.call(obj, 'zjy', 200);
//该语句使得Person构造函数中所有的this都指的是obj
//可以这样来理解:执行Person构造函数,构造函数的参数为call()的除第一个外的所有参数,这样就形成了一个新对象,然后这个新对象用obj(即call()的第一个参数)来接收
// Person(); //实际上该语句是这样执行的: Person.call(); 这样执行的构造函数的this指向Window(请看下面的引用部分)
</script>
- 【预编译时的this指向window】
<script type = "text/javascript"> function test() { console.log(this); } test(); </script>
原因:因为函数执行时,预编译形成的执行期上下文AO的最先的工作是——找形参变量声明arguments→预编译期每一个函数体里的this都指向window。
如果使用了call(), 那么预编译时的“this指向widow”就变成“this指向指定的内容”了。如:<script type = "text/javascript"> function test() { console.log(this); } test.call({name : 'sunny', age : 12}); </script>
- 【谁调用的函数,函数里的this就指向谁】
<script type = "text/javascript"> var obj = { name : 'obj', say : function() { console.log(this.name); } } obj.say(); </script>
如果使用call(),将obj.say();
改成obj.say.call(window);
:
显然,call()的作用就是改变this的指向。<script type = "text/javascript"> var obj = { name : 'obj', say : function() { console.log(this.name); } } var fun = obj.say; //表示“拿出函数体”,即fun仅仅代表一个函数体。 fun(); //函数自调用 </script>
如果使用call(),将fun();
改成fun.call(obj);
:
应用——“借用别人的构造函数来实现自己的功能”
<script type = "text/javascript">
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, sex, major, tel) {
Person.call(this, name, age, sex);
this.major = major;
this.tel = tel;
}
var stu = new Student('zjy', 200, '女', 'computer', '188');
</script>
call() 与 apply()
call() 与 apply()的作用都相同,不同之处在于两者传参列表:前者只能一个一个的传入构造函数的每个参数,而后者则需要以数组的形式传递一个构造函数所需的所有参数。
<script type = "text/javascript">
function Wheel(size, style) {
this.size = size;
this.style = style;
}
function Sit(quality, color) {
this.quality = quality;
this.color = color;
}
function Car(size, style, quality, color) {
Wheel.apply(this, [size, style]);
Sit.apply(this, [quality, color])
}
var car = new Car(111, 'cool', 'good', 'black');
</script>
【总结:call() 与 apply()的作用是改变this指向,不同之处在于传参列表。】