1 构造函数
ECMAScript中的构造函数是用于创建特定类型对象的。Object和Array是原生构造函数,运行时可以直接在执行环境中使用,也可以自定义构造函数。
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。任何函数只要使用new操作符调用就是构造函数,而不使用new操作符调用的函数就是普通函数。
按照惯例,构造函数名称的首字母要大写,非构造函数则以小写开头。
// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
2 理解原型
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性(指向原型对象)。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,指回与之关联的构造函数。就拿前面的例子来说, Person.prototype. constructor 指向 Person。然后,因构造函数而异,可能会给原型对象添加其他属性和方法。
(以上这段话光看有些难以理解,但是结合以下画图就好理解了)
在自定义的构造函数时,原型对象默认只会取得 constructor 属性;其他的所有方法都继承自Object 。每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值指向构造函数的原型对象。脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上暴露__proto__属性;通过这个属性可以访问对象的原型。在其他实现中,这个特性完全被隐藏了。关键在于理解这一点:实例与构造函数的原型对象之间有直接的联系,但实例与构造函数之间没有。
重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。、
实例只有指向原型的指针,没有指向构造函数的指针。
重写原型的正确方式:
function Person(){}
Person.prototype = {
// constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//恢复constructor属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
})
3 运算符
3.1 instance
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
(检查实例的原型链中是否包含指定构造函数的原型)
对以上代码的通俗理解:
创建Car构造函数,auto为Car原型对象的一个实例,auto实例的原型链中是否包含Car的原型,因为auto为Car原型对象的一个实例,即auto有一个指向Car原型对象的一个指针,所以auto实例的原型链中有包含Car的原型。
3.2 object
3.2.1 Object()
创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数
Object 构造函数将给定的值包装为一个新对象。
在非构造函数上下文中调用时, Object 和 new Object()表现一致。
3.2.2 Object.create()
使用指定的原型对象和属性创建一个新对象。
接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)
在只有一个参数时,Object.create()与某个object()方法效果相同。红宝书P245
替代Object.setPrototypeOf()->可能造成性能降低
3.2.3 判断属性存在于实例还是原型上
hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
(只有属性存在于实例上时才返回true)
hasPrototypeProperty() 属性存在于原型上时才返回true
4 盗用构造函数
基本思路:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象为上下文执行构造函数。(使用call实现继承)
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承 SuperType
SuperType.call(this);
//call中的this指针指向SuperType,先改为指向this,并执行SuperType()
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
https://www.jianshu.com/p/43a996db58e7
call方法的作用:
函数名.call(对象,arg1…argn)
1.将this指向第一个参数
2.将第二个及以后所有的参数,作为实参传递给函数
(让原型上的 call 方法执行,在执行 call 方法时,让调用 call 的方法中的 this 指向 call 方法里的第一个参数,再执行调用 call 的这个方法。)
注意事项
1.call和apply只是传参方式的区别,apply可以通过数组的方式传参
2.call和apply的第一个参数如果是null和underfined,那么this会指向windows
3.call和apply的第一个参数如果为值类型的数据,那么会将这个值类型的数据,转换成其对应的引用类型的数据,然后再将this指向这个引用类型的数据
4.call和apply立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。
5.当参数的个数确定的情况下可以使用call;当参数的个数不确定的情况下可以使用apply
5 组合继承
基本的思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
保留instance操作符和isPrototypeOf()方法识别合成对象的能力。
6 总结
1.原型链实现继承,缺点:但是包含引用值时,原型实际上变成了另一个类型的实例,即原先的实例属性变成了原型属性。
2.盗用构造函数解决了原型包含引用值导致的继承问题,
缺点:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
(也是使用构造函数模式自定义类型的问题)
3.组合继承弥补了原型链和盗用构造函数的不足。