类型
基本类型 number,string,boolean,undefined,null,Symbol(es 6),BigInt(es 10)
引用类型 object
JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()) ,推荐使用Object.prototype.toString.call()
1.typeof
typeof
操作符返回一个字符串,表示未经计算的操作数的类型。
typeof 返回值(‘number’|’string’|’boolean’|’undefined’|’symbol’|’bigInt’|’object’|’function’),返回值八种字符串;其中typeof null===’object’
只能准确判断基本数据类型,无法准确判断引用类型,如{},[]
console.log(typeof 2); // number
console.log(typeof 'str'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof Symbol()) // Symbol
console.log(typeof BigInt()) // BigInt
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null) // object
console.log(typeof function(){}); // function
console.log(typeof new Number(2)); // object
复制代码
2.instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
instanceof 返回一个boolean值,
无法准确判断基本数据类型都是false;只能准确判断引用类型
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
// console.log(undefined instanceof Undefined); ReferenceError
// console.log(null instanceof Null); ReferenceError
复制代码
3.constructor
constructor 返回构造函数
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
//这里有一个坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
复制代码
4.Object.prototype.toString.call
返回字符串,能够准确的判断任何类型
Object.prototype.toString.call(1) //[object Number]
Object.prototype.toString.call('') //[object String]
Object.prototype.toString.call(false) //[object Boolean]
Object.prototype.toString.call([]) //[object Array]
Object.prototype.toString.call({}) //[object Object]
Object.prototype.toString.call(new Date()) //[object Date]
Object.prototype.toString.call(undefined) //[object Undefined]
Object.prototype.toString.call(null) //[object Null]
Object.prototype.toString.call(new Number(1)) //[object Number]
复制代码
对象的创建
#### 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("james",9,"student");
var person2 = createPerson("kobe",9,"student");
复制代码
2.构造函数模式
优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。
function createPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new createPerson("james",9,"student");
var person2 = new createPerson("kobe",9,"student");
复制代码
3.原型模式
优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。
缺点:
- 首先第一个问题是原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。
- 因为所有的实例都是共享一组属性,对于包含基本值的属性来说没有问题,但是对于包含引用类型的值来说(例如数组对象),所有的实例都是对同一个引用类型进行操作,那么属性的操作就不是独立的,最后导致读写的混乱。我们创建的实例一般都是要有属于自己的全部属性的,因此单独使用原型模式的情况是很少存在的。
unction Person(){
}
Person.prototype.name = "james";
Person.prototype.age = 9;
Person.prototype.job = "student";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // "james"
var person2 = new Person();
person2.sayName(); // "james"
console.log(person1.sayName === person2.sayName) // true
复制代码
4.组合使用构造函数模式和原型模式
优点:采用了构造函数模式和原型模式的优点,这种混成模式是目前使用最广泛,认同度最高的一种创建自定类型的方法。
缺点:由于使用了两种模式,因此对于代码的封装性来说不是很好。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
}
var person1 = new createPerson("james",9,"student");
var person2 = new createPerson("kobe",9,"student");
console.log(person1.name); // "james"
console.log(person2.name); // "kobe"
console.log(person1.sayName === person2.sayName); // true
复制代码
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);
}
}
}
var person1 = new createPerson("james",9,"student");
person1.sayName(); // "james"
复制代码
6.寄生构造函数模式
优点:我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。
缺点:和工厂模式一样的问题,不能依赖 instanceof 操作符来确定对象的类型。
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("james",9,"student");
复制代码
7.稳妥构造函数模式
优点:以上面为例,除了 sayName 方法外,没有别的方法可以访问数据成员,这就是稳妥构造函数提供的安全性。
缺点:和寄生构造函数一样,没有办法使用 instanceof 操作符来判断对象的类型
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
console.log(this.name);
}
//返回对象
return o;
}
var person1 = Person("james",9,"student");
person1.sayName(); // "james"
复制代码
继承
1.原型链
在 ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。这一部分已经在前面总结过了,就不再多说了。
缺点:
- 包含引用类型的原型属性会被所有实例属性共享,容易造成属性的修改混乱。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
基于以上问题,在实践中很少会单独使用原型链。
2.借用构造函数
优点:可以在子类型构造函数中向超类型构造函数添加参数
缺点:和构造函数模式一样的问题,所有的方法都在构造函数中定义,因此就无法做到函数的复用。而且超类型的原型中定义的方法,对于子类型而言也是不可见的。
基于以上问题,借用构造函数的技术也是很少单独使用的。
3.组合继承
组合继承指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。这种方法的主要思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。如下面的例子所示:
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.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType("james",9);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); // "james"
instance1.sayAge(); // 9
var instance2 = new SubType("kobe",10);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); // "kobe"
instance2.sayAge(); // 10
复制代码
优点:组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPropertyOf() 也能够用于识别基于组合继承创建的对象。
缺点:调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。
4.原型式继承
原型式继承的主要思路是可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。如下面的例子所示
function object(o){
function F(){};
F.prototype = o;
return new F();
}
复制代码
简单来说这个函数的作用就是,传入一个对象,返回一个原型对象为该对象的新对象。
ECMAScript 5中新增了 Object.create() 方法规范了原型式继承。这个方法接收两个参数,一个是将被用作新对象原型的对象,一个是为新对象定义额外属性的对象(可选)。
注意第二个参数的格式与 Object.defineProperties() 方法的第二个参数格式相同。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。在第二个参数为空的情况下,该方法与 object() 方法的行为相同。
优点:可以实现基于一个对象的简单继承,不必创建构造函数
缺点:与原型链中提到的缺点相同,一个是传参的问题,一个是属性共享的问题
5.寄生式继承
寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。如下面的例子所示
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ // 某种方式增强这个对象
console.log("hi");
}
return clone; // 返回这个对象
}
var person = {
name: "james"
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
复制代码
优点: 在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。
缺点:使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。
6.寄生式组合继承
前面我们提到过了组合继承的缺点,由于调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。
寄生式组合继承就是用来解决这个问题,它与组合继承不同的地方主要是,在继承原型时,我们继承的不是超类的实例对象,而是原型对象是超类原型对象的一个实例对象,这样就解决了基类的原型对象中增添了不必要的超类的实例对象中的所有属性d的问题。
我们可以封装继承原型时函数如下:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建原型对象是超类原型对象的一个实例对象
prototype.constructor = subType; // 弥补因为重写原型而失去的默认的 constructor 属性。
subType.prototype = prototype; // 实现原型继承
}
复制代码
优点:效率高,避免了在 SubType.prototype 上创建不必要的属性。与此同时还能保持原型链不变,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。