1.undefined
一个变量声明了但没有初始化,那么这个变量是undefined
。它与没有声明的变量是不同的。
注意:typeof
对声明了但没有初始化的变量和没有声明的变量的结果都是undefined
。
#2. NULL
Null
表示的是一个空对象指针,用typeof
来检测null
的值会返回object
3.String
##3.1 toString()和String()
toString()
可以把一个值转换为字符串,但是转换不了null
和undefined
String()
在转换其他值的时候和toString()
是一样的,也可以把null和undefined
转换成字符串
4.创建对象
4.1工厂模式
用一个函数封装以特定接口创建函数的细节,根据接收来的参数创建一个包含所有必要信息的对象。
function createPerson(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 person1 = createPerson('aa', 12, 'student');
var person2 = createPerson('bb', 34, 'teacher');
工厂模式解决了创建多个相似对象的问题,但没有解决对象识别的问题。
4.2构造函数模式
构造函数可以用来创建特定类型的对象。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person('aa', 12, 'student');
var person2 = new Person('bb', 34, 'teacher');
构造函数始终以大写字母开头,
Person
可以看出构造函数Person()和工厂模式createPerson()的区别如下:
- 没有显示的创建对象;
- 直接将属性和方法赋给了this对象
- 没有return语句
创建一个Person实例,需要以new
操作符,调用构造函数经历以下步骤: - 创建一个新对象
- 将构造函数的作用域赋给新对象,即
this
指向新对象 - 执行构造函数中的代码,即为新对象添加属性
- 返回新对象
上面的例子中,person1和person2都有一个constructor的属性,都执行Person
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
用instanceof检测对象类型的时候,发现他们即是Object类型,又是Person类型
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
这就是构造函数模式胜过工厂模式的地方,可以将实例标识为一种特定的类型
构造函数存在的问题:
每个方法都要在每个实例上重新创建一遍,如person1和person2中的sayName()函数
可以把函数定义转移到构造函数外部解决这个问题
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person('aa', 12, 'student');
var person2 = new Person('bb', 34, 'teacher');
这样person1和person2就共享了在全局作用域中定义的sayName()函数
但问题是,如果对象需要很多方法,就要定义很对全局函数
4.3原型模式
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个兑现个实例的原型对象。原型对象可以让所有对象实例共享它所包含的属性和方法。换句话说,不需要再构造函数中定义兑现实例的信息,而是可以将这些信息添加到原型对象中。
function Person(name, age, job) {
}
Person.prototype.name = 'aa';
Person.prototype.age = 12;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //aa
var person2 = new Person();
person2.sayName(); //aa
alert(person1.sayName == person2.sayName);//true
4.3.1理解原型对象
只要创建了一个函数,这个函数就会包含一个prototype属性,指向函数的原型对象。原型函数有一个constructor属性,指向prototype所在的函数。
当调用构造函数创建一个实例后,实例内部包含一个指针,执行构造函数的原型对象。可以通过isPrototypeOf()来确定这个关系。
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true
也可以通过Object.getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//aa
当代码在读取某个对象的某个属性的时候,先从对象实例本身开始,找到了则返回属性的值;如果没找到,则继续搜索指针对象指向的原型对象,找到了,返回值。
可以通过对象实例访问保存在原型中的值,却不能通过对象重写原型中的值
function Person(name, age, job) {
}
Person.prototype.name = 'aa';
Person.prototype.age = 12;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'bb';
person1.sayName(); //bb
person2.sayName(); //aa
实例中修改属性,会在实例中覆盖原型属性的值,但不会修改原型属性的值
使用delete可以永久删除实例属性
function Person(name, age, job) {
}
Person.prototype.name = 'aa';
Person.prototype.age = 12;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'bb';
person1.sayName(); //bb
person2.sayName(); //aa
delete person1.name;
person1.sayName(); //aa
使用hasOwnProperty()方法检测一个属性是否存在于实例中,只有在实例中才会返回true
4.3.2原型与in操作符
in操作符重要通过对象能够访问到属性时就返回true,无论是原型还是实例中
可以定义一个方法判断是不是在原型中
function hasPrototypeProperty(object, name) {
return !object.hasOwnProterty(name) && (name in object);
}
只在原型里才会返回true
function Person(name, age, job) {
}
Person.prototype.name = 'aa';
Person.prototype.age = 12;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty('name')); //false
alert('name' in person1); //true
person1.name = 'bb';
alert(person1.hasOwnProperty('name')); //true
alert('name' in person1); //true
var person = new Person();
alert(hasPrototypeProperty(person, 'name')); //true
person.name = 'bb';
alert(hasPrototypeProperty(person, 'name')); //false
要取得对象上所有可枚举的实例属性,可以使用Object.keys()
方法。接受一个对象作为参数,返回包含所有可枚举属性的字符串数组。
function Person(name, age, job) {
}
Person.prototype.name = 'aa';
Person.prototype.age = 12;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
alert(this.name);
}
var keys = Object.keys(Person.prototype);
console.log(keys); // ["name", "age", "job", "sayName"]
var person1 = new Person();
person1.name = 'bb';
person1.age = '44';
var person1Key = Object.keys(person1);
console.log(person1Key); //["name", "age"]
如果想获取所有的属性,无论是否可枚举,可以用Object.getOwnPropertyNames()
var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);// ["constructor", "name", "age", "job", "sayName"]
4.3.3更简单的原型语法
用包含所有属性的和方法的对象字面量来重写整个原型
function Person(name, age, job) {
}
Person.prototype = {
name: 'aa',
age: 12,
job: 'student',
sayName: function() {
alert(this.name);
}
}
上面的方法,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。那么constructor属性就不在指向Person了,而是指向了Object构造函数,如果constructor的值很重要,可以特意设回原来的值
function Person(name, age, job) {
}
Person.prototype = {
constructor: Person,
name: 'aa',
age: 12,
job: 'student',
sayName: function() {
alert(this.name);
}
}
但这也到导致一个问题,原本constructor属性是不可枚举的,这种方式设置完之后就会变成可枚举的,但是也可以通过Object.defineProperty设为不可枚举
function Person(name, age, job) {
}
Person.prototype = {
name: 'aa',
age: 12,
job: 'student',
sayName: function() {
alert(this.name);
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
还有一个问题是,如果先创建一个实例,再以对象字面量的方法重写了原型,就切断了实例与原型对象之间的联系,实例指向的还是最初的原型对象。
function Person(name, age, job) {
}
var person1 = new Person();
Person.prototype = {
constructor: Person,
name: 'aa',
age: 12,
job: 'student',
sayName: function() {
alert(this.name);
}
}
person1.sayName(); //error
4.3.4原生对象的原型
所有的原生引用类型(Object,Array,String )都在其构造函数的原型上定义了方法。例如,在Arrray.prototype()中可以找到sort()方法,在String.prototype中()可以找到substring()方法
也可以通过原型添加新的方法
4.3.5原型对象的问题
当原型中包含引用类型值的时候,问题比较大
function Person(name, age, job) {
}
Person.prototype = {
constructor: Person,
name: 'aa',
age: 12,
job: 'student',
friends: ['cc', 'dd'],
sayName: function() {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('ee');
console.log(person1.friends);//["cc", "dd", "ee"]
console.log(person2.friends);//["cc", "dd", "ee"]
person1中的改变会反应到person2中,实例一般要有自己的属性,所有很少单独使用原型模式。
4.4组合使用构造函数模式和原型模式(最常见的方式)
构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性。这样每个函数都会有自己的一份实例属性的副本,但同时又共享对方法的引用。也可以向构造函数种传递参数。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['cc', 'dd'];
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
}
var person1 = new Person('aa', 12, 'student');
var person2 = new Person('bb', 34, 'teacher');
person1.friends.push('ee');
console.log(person1.friends);//["cc", "dd", "ee"]
console.log(person2.friends);//["cc", "dd"]
console.log(person1.friends == person2.friends); //false
console.log(person1.sayName == person2.sayName); //true
4.5动态原型模式
把所有的信息都封装在构造函数中,通过在构造函数中初始化原型,同时保持了构造函数和原型的优点。可以通过检查某个应该存在的方法是狗有效,来决定是否需要初始化原型
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);
}
}
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
}
var person1 = new Person('aa', 12, 'student');
person1.sayName();
动态原型模式不能使用对象字面量重写原型,因为在已经创建了实例的情况下重写原型,会切断现有实例与原型之间的联系。
4.6寄生构造函数模式
创建一个函数,封装创建对象的代码,再返回新创建的对象。
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 person1 = new Person('aa', 12, 'student');
person1.sayName();
这个模式可以在特殊情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,就可以使用这个模式。
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');
console.log(colors.toPipedString()); // red|blue|green
返回的对象与构造函数或者构造函数的原型属性之间没有关系
构造函数返回的对象与外部创建的对象没有什么不同
不能用instanceof
来确定对象的类型
4.7稳妥构造函数
稳妥对象值得是没有公共属性,其方法也不引用this的对象
注意:
- 创建随想的实例方法不引用this
- 不使用new操作符调用构造函数
function Person(name, age, job) {
var o = new Object();
o.sayName = function() {
alert(this.name);
}
return o;
}
var person1 = new Person('aa', 12, 'student');
person1.sayName();
只有使用sayName(),其他方法均访问不到name的值。这样person1中保存的是稳妥对象。
5.继承
5.1原型链
原型链式实现继承的主要方法,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现的本质是重写原型对象,代之一个新类型的实例。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSubValue());//false
alert(instance.getSuperValue());//true
5.1.1别忘记默认的原型
所有函数的默认原型都是Object的实例,这就是为什么所有自定义的类型都会继承toString(),valueOf()等默认方法的原因。
5.1.2确定原型和实例的关系
- 第一种
instanceof
操作符
alert(instance instanceof Object);//true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true
- 第二种
isPrototypeOf()
方法
alert(Object.prototype.isPrototypeOf(instance));//true
alert(SuperType.prototype.isPrototypeOf(instance));//true
alert(SubType.prototype.isPrototypeOf(instance));//true
5.1.3谨慎的定义方法
子类型有时候需要重写超类型中的方法或者添加超类型中不存在的方法。给原型添加方法一定的代码一定要放在替换原型之后
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
// 重写方法
SubType.prototype.getSuperValue = function() {
return false;
}
var instance = new SubType();
alert(instance.getSuperValue());//false
还有一点要注意在使用原型链继承的时候,不能使用对象字面量创建原型方法,因为这样会重写原型链
5.1.4原型链的问题
包含引用值的原型会被所有的实例共享。这就是为什么在构造函数中而不再原型对象中定义属性的原因
5.2借助构造函数
为了解决在原型中包含引用类性值所带来的问题,采用借助构造函数的技术,也叫做伪造对象或经典继承。思想是在子类型构造函数的内部调用超类型构造函数。通过apply()
或者call()
function SuperType() {
this.color = ['red', 'blue', 'green'];
}
function SubType() {
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push('black');
alert(instance1.color); //red,blue,green,black
var instance2 = new SubType();
alert(instance2.color); //red,blue,green
在SubType对象上执行SuperType函数中定义的所有对象初始化代码。这样SubType的每个实例就都会有自己的color属性的副本了。
5.2.1传递参数
借助构造函数的优势是可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name) {
this.name = name;
}
function SubType() {
SuperType.call(this,'aa');
this.age = 22;
}
var instance1 = new SubType();
alert(instance1.name); //aa
alert(instance1.age); //29
在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了
name
属性
5.2.2构造函数的问题
考虑到复用问题,构造函数很少单独使用
5.3组合继承(最常用的继承模式)
将原型链和借助构造函数的技术组合到一起。思路是使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。即通过在原型上定义方法实现了函数复用,又能够保证每个实例有自己的属性。
function SuperType(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance1 = new SubType('aa', 29);
instance1.color.push('black');
alert(instance1.color); //red,blue,green,black
instance1.sayName(); //aa
instance1.sayAge(); //29
var instance2 = new SubType('bb', 11);
alert(instance2.color); //red,blue,green
instance2.sayName();//bb
instance2.sayAge();//11
5.4原型式继承
借助原型,基于已有的对象创建对象,不必因此创建自定义的类型
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'aa',
friends: ['bb', 'cc', 'dd']
}
var anotherPerson = object(person);
anotherPerson.name = 'gg';
anotherPerson.friends.push('rr');
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'll';
yetAnotherPerson.friends.push('pp');
alert(person.friends); //bb,cc,dd,rr,pp
要求有一个对象可以作为另一个对象的基础。把person对象传入object(),返回一个新对象,这个新对象把person作为原型,包含一个基本类型属性和引用类型属性,friends会被共享。
Object.create()
方法可以代替上面的object()
方法,Object.create()
的第二个参数是可选的,可以传入自定义的属性,可以覆盖原型兑现上的属性。
var person = {
name: 'aa',
friends: ['bb', 'cc', 'dd']
}
var anotherPerson = Object.create(person, {
name: {
value: 'gg'
}
})
alert(anotherPerson.name); //gg
5.5寄生式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert('hi');
}
return clone;
}
var person = {
name: 'aa',
friends: ['bb', 'cc', 'dd']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //gg
5.6寄生组合式继承
组合继承最大的问题是无论在什么时候都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型的构造函数内部。
function SuperType(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
SuperType.call(this, name); //第二次调用SuperType
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}
上面调用了两次超类型函数,第一次调用name和color是SubType原型属性,第二次是实例属性。所有有两组name和color。
寄生组合式继承思路是不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非是超类型的副本。就是使用寄生式继承来继承超类型的原型,然后再讲结果指定给子类型的原型。
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //创建超类型原型的副本
prototype.constructor = subType;//弥补重写原型而失去的constructor属性
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
alert(this.age);
}
var instance1 = new SubType('aa', 29);
instance1.sayAge(); //29
只调用了一次SuperType构造函数,并且避免了在SubType.prototype上面创建不必要的,多余的属性。