JS的判断类型

typeof

ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。我们都知道可以使用typeof运算符求得一个变量的类型,但是对引用类型变量却只会返回object,也就是说typeof只能正确识别基本类型值变量。

var a = "abc";
console.log(typeof a); // "string"
var b = 123;
console.log(typeof b); // "number"
var c = true;
console.log(typeof c); // "boolean"
var d = null;
console.log(typeof d); // "object"
var f = undefined;
console.log(typeof f); // "undefined"
var g;
console.log(typeof g); // "undefined"
console.log(typeof x); // "undefined"

您也许会问,为什么 typeof 运算符对于 null 值会返回 "object"。这实际上是 JavaScript 最初实现中的一个错误,然后被 ECMAScript 沿用了。现在,null 被认为是对象的占位符,从而解释了这一矛盾,但从技术上来说,它仍然是原始值。

最后一个比较奇怪,typeof一个不存在的变量x居然返回了"object"而不是"undefined"。
我们在来如下代码:

var a = function() { };
console.log(typeof a); // "function"
var b = [1,2,3]; 
console.log(typeof b); // "object"
var c = { };
console.log(typeof c); // "object"

对于数组和对象都返回"object",因此我们日常开发中一个常见需求就是如何判断变量是数组还是对象。

类型判断

类型判断,一般就是判断是否是数组,是否是空对象。这是针对这个需求,我日常用过或见过的判断方法

判断是否是数组

有数组:var a = [1,2,3,4,5];
方法一:

toString.call(a); // "[object Array]"
方法二:

a instanceof Array; //true
方法三:

a.constructor == Array; //true

第一种方法比较通用,也就是Object.prototype.toString.call(a)的简写。

instanceof和constructor判断的变量,必须在当前页面声明的,比如,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个a,并将其赋值给父页面的一个变量,这时判断该变量,Array == object.constructor会返回false;

var a = [1,2,3,4,5];
console.log(toString.call(a)); // "[object Array]"                     
console.log(a instanceof Array); //true
console.log(a.constructor == Array); //true

判断是否是空对象

有变量:var obj = {};

方法一:

JSON.stringify(obj); // "{}"
通过转换成JSON对象来判断是否是空大括号

方法二:

if(obj.id){ //如果属性id存在....}
这个方法比较土,大多数人都能想到,前提是得知道对象中有某个属性。

方法三:

function isEmptyObject(e) { var t; for (t in e) return !1; return !0 } //trueisEmptyObject(obj); //falseisEmptyObject({ "a":1, "b":2});
这个方法是jQuery的isEmptyObject()方法的实现方式。

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

JavaScript Demo: Expressions - instanceof

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car);
// expected output: true

console.log(auto instanceof Object);
// expected output: true

语法

object instanceof constructor

参数

object
某个实例对象
constructor
某个构造函数

描述

instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

// 定义构造函数
function C(){} 
function D(){} 

var o = new C();


o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

需要注意的是,如果表达式 obj instanceof Foo 返回 true,则并不意味着该表达式会永远返回 true,因为 Foo.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 obj 的原型链上,这时原表达式的值就会成为 false。另外一种情况下,原表达式的值也会改变,就是改变对象 obj 的原型链的情况,虽然在目前的ES规范中,我们只能读取对象的原型而不能改变它,但借助于非标准的 proto 伪属性,是可以实现的。比如执行 obj.proto = {} 之后,obj instanceof Foo 就会返回 false 了。

instanceof 和多全局对象(例如:多个 frame 或多个 window 之间的交互)

在浏览器中,我们的脚本可能需要在多个窗口之间进行交互。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。这可能会引发一些问题。比如,表达式 [] instanceof window.frames[0].Array 会返回 false,因为 Array.prototype !== window.frames[0].Array.prototype,并且数组从前者继承。

起初,你会认为这样并没有意义,但是当你在你的脚本中开始处理多个 frame 或多个 window 以及通过函数将对象从一个窗口传到另一个窗口时,这就是一个有效而强大的话题。比如,实际上你可以通过使用Array.isArray(myObj) 或者Object.prototype.toString.call(myObj) === "[object Array]" 来安全的检测传过来的对象是否是一个数组。

比如检测一个 Nodes 在另一个窗口中是不是 SVGElement,你可以使用myNode instanceof myNode.ownerDocument.defaultView.SVGElement

示例

演示 String 对象和 Date 对象都属于 Object 类型和一些特殊情况

下面的代码使用了 instanceof 来证明:String 和 Date 对象同时也属于Object 类型(他们是由 Object 类派生出来的)。

但是,使用对象文字符号创建的对象在这里是一个例外:虽然原型未定义,但 instanceof Object 返回 true。

var simpleStr = "This is a simple string"; 
var myString  = new String();
var newStr    = new String("String created with constructor");
var myDate    = new Date();
var myObj     = {};
var myNonObj  = Object.create(null);

simpleStr instanceof String; // 返回 false, 检查原型链会找到 undefined
myString  instanceof String; // 返回 true
newStr    instanceof String; // 返回 true
myString  instanceof Object; // 返回 true

myObj instanceof Object;    // 返回 true, 尽管原型没有定义
({})  instanceof Object;    // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法

myString instanceof Date; //返回 false

myDate instanceof Date;     // 返回 true
myDate instanceof Object;   // 返回 true
myDate instanceof String;   // 返回 false

演示 mycar 属于 Car 类型的同时又属于 Object 类型

下面的代码创建了一个类型 Car,以及该类型的对象实例 mycar. instanceof 运算符表明了这个 mycar 对象既属于 Car 类型,又属于 Object 类型。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var mycar = new Car("Honda", "Accord", 1998);
var a = mycar instanceof Car;    // 返回 true
var b = mycar instanceof Object; // 返回 true

不是XXX的实例
要检测对象不是某个构造函数的实例时,你可以这样做

if (!(mycar instanceof Car)) {
  // Do something, like mycar = new Car(mycar)
}

这和以下代码完全不同

if (!mycar instanceof Car)

这段代码永远会得到 false(!mycar 将在 instanceof 之前被处理,所以你总是在验证一个布尔值是否是 Car 的一个实例)。


Object.is()

Object.is() 方法判断两个值是否是相同的值

语法

Object.is(value1, value2);

参数

value1
第一个需要比较的值。
value2
第二个需要比较的值。

返回值

表示两个参数是否相同的布尔值

描述

Object.is() 判断两个值是否相同。如果下列任何一项成立,则两个值相同:

  • 两个值都是 undefined
  • 两个值都是 null
  • 两个值都是 true 或者都是 false
  • 两个值是由相同个数的字符按照相同的顺序组成的字符串
  • 两个值指向同一个对象
  • 两个值都是数字并且
    • 都是正零 +0
    • 都是负零 -0
    • 都是 NaN
    • 都是除零和 NaN 外的其它同一个数字

这种相等性判断逻辑和传统的 == 运算不同,== 运算符会对它两边的操作数做隐式类型转换(如果它们类型不同),然后才进行相等性比较,(所以才会有类似 "" == false 等于 true 的现象),但 Object.is 不会做这种类型转换。

这与 === 运算符的判定方式也不一样。=== 运算符(和== 运算符)将数字值 -0+0 视为相等,并认为 Number.NaN 不等于 NaN

例子

Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

Object.is('foo', 'bar');     // false
Object.is([], []);           // false

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is(null, null);       // true

// 特例
Object.is(0, -0);            // false
Object.is(0, +0);            // true
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true

JavaScript 中的相等性判断

ES2015中有四种相等算法:

  • 非严格相等比较 (==)
  • 严格相等比较 (===): 用于 Array.prototype.indexOf, Array.prototype.lastIndexOf, 和 case-matching
  • 同值零: 用于 %TypedArray%ArrayBuffer构造函数、以及MapSet操作, 并将用于 ES2016/ES7 中的String.prototype.includes
  • 同值: 用于所有其他地方

JavaScript提供三种不同的值比较操作:

  • 严格相等 ("triple equals" 或 "identity"),使用 === ,
  • 宽松相等 ("double equals") ,使用 ==
  • 以及 Object.is (ECMAScript 2015/ ES6 新特性)

选择使用哪个操作取决于你需要什么样的比较。

简而言之,在比较两件事情时,双等号将执行类型转换; 三等号将进行相同的比较,而不进行类型转换 (如果类型不同, 只是总会返回 false ); 而Object.is的行为方式与三等号相同,但是对于NaN和-0和+0进行特殊处理,所以最后两个不相同,而Object.is(NaN,NaN)将为 true。(通常使用双等号或三等号将NaN与NaN进行比较,结果为false,因为IEEE 754如是说.) 请注意,所有这些之间的区别都与其处理原语有关; 这三个运算符的原语中,没有一个会比较两个变量是否结构上概念类似。对于任意两个不同的非原始对象,即便他们有相同的结构, 以上三个运算符都会计算得到 false 。

严格相等 ===

全等操作符比较两个值是否相等,两个被比较的值在比较前都不进行隐式转换。如果两个被比较的值具有不同的类型,这两个值是不全等的。否则,如果两个被比较的值类型相同,值也相同,并且都不是 number 类型时,两个值全等。最后,如果两个值都是 number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0 和 -0 时,两个值被认为是全等的。

var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true

console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false

在日常中使用全等操作符几乎总是正确的选择。对于除了数值之外的值,全等操作符使用明确的语义进行比较:一个值只与自身全等。对于数值,全等操作符使用略加修改的语义来处理两个特殊情况:第一个情况是,浮点数 0 是不分正负的。区分 +0 和 -0 在解决一些特定的数学问题时是必要的,但是大部分情况下我们并不用关心。全等操作符认为这两个值是全等的。第二个情况是,浮点数包含了 NaN 值,用来表示某些定义不明确的数学问题的解,例如:正无穷加负无穷。全等操作符认为 NaN 与其他任何值都不全等,包括它自己。(等式 (x !== x) 成立的唯一情况是 x 的值为 NaN)

非严格相等 ==

相等操作符比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。在转换后(等式的一边或两边都可能被转换),最终的比较方式等同于全等操作符 === 的比较方式。 相等操作符满足交换律。

相等操作符对于不同类型的值,进行的比较如下图所示:


photo 1.png

在上面的表格中,ToNumber(A) 尝试在比较前将参数 A 转换为数字,这与 +A(单目运算符+)的效果相同。ToPrimitive(A)通过尝试调用 A 的A.toString() 和 A.valueOf() 方法,将参数 A 转换为原始值(Primitive)。

一般而言,根据 ECMAScript 规范,所有的对象都与 undefined 和 null 不相等。但是大部分浏览器允许非常窄的一类对象(即,所有页面中的 document.all 对象),在某些情况下,充当效仿 undefined 的角色。相等操作符就是在这样的一个背景下。因此,IsFalsy(A) 方法的值为 true ,当且仅当 A 效仿 undefined。在其他所有情况下,一个对象都不会等于 undefined 或 null。

var num = 0;
var obj = new String("0");
var str = "0";
var b = false;

console.log(num == num); // true
console.log(obj == obj); // true
console.log(str == str); // true

console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
console.log(null == undefined); // true

// both false, except in rare cases
console.log(obj == null);
console.log(obj == undefined);

同值相等

同值相等解决了最后一个用例:确定两个值是否在任何情况下功能上是相同的。(这个用例演示了里氏替换原则的实例。)当试图对不可变(immutable)属性修改时发生出现的情况:

// 向 Nmuber 构造函数添加一个不可变的属性 NEGATIVE_ZERO
Object.defineProperty(Number, "NEGATIVE_ZERO",
                      { value: -0, writable: false, configurable: false, enumerable: false });

function attemptMutation(v)
{
  Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}

Object.defineProperty 在试图修改不可变属性时,如果这个属性确实被修改了则会抛出异常,反之什么都不会发生。例如如果 v 是 -0 ,那么没有发生任何变化,所以也不会抛出任何异常。但如果 v 是 +0 ,则会抛出异常。不可变属性和新设定的值使用 same-value 相等比较。

同值相等由 Object.is方法提供。

零值相等

与同值相等类似,不过会认为 +0 与 -0 相等。

理解相等比较的模型

在 ES2015 以前,你可能会说双等和三等是“扩展”的关系。比如有人会说双等是三等的扩展版,因为他处理三等所做的,还做了类型转换。例如 6 == "6" 。反之另一些人可能会说三等是双等的扩展,因为他还要求两个参数的类型相同,所以增加了更多的限制。怎样理解取决于你怎样看待这个问题。

但是这种比较的方式没办法把 ES2015 的 Object.is 排列到其中。因为 Object.is 并不比双等更宽松,也并不比三等更严格,当然也不是在他们中间。从下表中可以看出,这是由于Object.is 处理 NaN 的不同。注意假如 Object.is(NaN, NaN) 被计算成 false ,我们就可以说他比三等更为严格,因为他可以区分 -0+0 。但是对 NaN 的处理表明,这是不对的。Object.is 应该被认为是有其特殊的用途,而不应说他和其他的相等更宽松或严格。

判等1.png

判等2.png

什么时候使用 Object.is 或是三等

总的来说,除了对待NaN 的方式,Object.is 唯一让人感兴趣的,是当你需要一些元编程方案时,它对待0的特殊方式,特别是关于属性描述器,即你的工作需要去镜像Object.defineProperty 的一些特性时。如果你的工作不需要这些,那你应该避免使用Object.is,使用===来代替。即使你需要比较两个NaN 使其结果为true,总的来说编写使用NaN 检查的特例函数(用旧版本ECMAScript的isNaN方法)也会比想出一些计算方法让Object.is不影响不同符号的0的比较更容易些。


Array.is()

Array.isArray()用于确定传递的值是否是一个 Array

Array.isArray([1, 2, 3]);  
// true
Array.isArray({foo: 123}); 
// false
Array.isArray("foobar");   
// false
Array.isArray(undefined);  
// false

语法

Array.isArray(obj)

参数

obj
需要检测的值。

返回值

如果值是 Array,则为true; 否则为false。

示例

// 下面的函数调用都返回 true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
Array.isArray(new Array('a', 'b', 'c', 'd'))
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype); 

// 下面的函数调用都返回 false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);
Array.isArray(false);
Array.isArray(new Uint8Array(32))
Array.isArray({ __proto__: Array.prototype });

instanceof 和 isArray

当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes.

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

Polyfill

假如不存在 Array.isArray(),则在其他代码之前运行下面的代码将创建该方法。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

Object.prototype.toString()

function Dog(name) {
  this.name = name;
}

const dog1 = new Dog('Gabby');

Dog.prototype.toString = function dogToString() {
  return '' + this.name;
}

console.log(dog1.toString());
// expected output: "Gabby"

语法

obj.toString()

返回值

一个表示该对象的字符串。

描述

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。以下代码说明了这一点:

var o = new Object();
o.toString(); // returns [object Object]

示例

可以自定义一个方法,来取代默认的 toString() 方法。该 toString() 方法不能传入参数,并且必须返回一个字符串。自定义的 toString() 方法可以是任何我们需要的值,但如果它附带有关对象的信息,它将变得非常有用。

以下代码定义了 Dog 对象类型,并创建了一个 Dog 类型的 theDog 对象:

function Dog(name,breed,color,sex) {
  this.name = name;
  this.breed = breed;
  this.color = color;
  this.sex = sex;
}

var theDog = new Dog("Gabby", "Lab", "chocolate", "female");

如果当前的对象调用了 toString() 方法,它将会返回从 Object继承而来的 toString() 方法的返回默认值:

theDog.toString(); // 返回 [object Object]

下面的代码中定义了一个叫做 dogToString() 的方法来覆盖默认的 toString() 方法。这个方法生成一个 "property = value;" 形式的字符串,该字符串包含了当前对象的 name、breed、color 和 sex 的值。

Dog.prototype.toString = function dogToString() {
 var ret = "Dog " + this.name + " is a " + this.sex + " " + this.color + " " + this.breed;
 return ret;
}

也可以这样写:

Dog.prototype.toString = function dogToString() {
  return `Dog ${this.name} is a ${this.sex} ${this.color} ${this.breed}`;
}

使用上述代码,任何时候在字符串上下文中使用 theDog.toString() 时,JavaScript 都会自动调用 dogToString() 方法(dogToString() 可以是一个匿名函数),并且返回以下字符串:

"Dog Gabby is a female chocolate Lab"

使用 toString() 检测对象类型

可以通过 toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg。

var toString = Object.prototype.toString;

toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]

//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]

同样是检测对象obj调用toString方法(关于toString()方法的用法的可以参考toString的详解),obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

    这是因为toString为Object的原型方法,而Array 、Function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串.....),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object上原型toString方法。

我们可以验证一下,将数组的toString方法删除,看看会是什么结果:

var arr=[1,2,3];
console.log(Array.prototype.hasOwnProperty("toString"));//true
console.log(arr.toString());//1,2,3
delete Array.prototype.toString;//delete操作符可以删除实例属性
console.log(Array.prototype.hasOwnProperty("toString"));//false
console.log(arr.toString());//"[object Array]"

删除了Array的toString方法后,同样再采用arr.toString()方法调用时,不再有屏蔽Object原型方法的实例方法,因此沿着原型链,arr最后调用了Object的toString方法,返回了和Object.prototype.toString.call(arr)相同的结果。


Javascript构造函数及原型对象

使用Object或对象字面量创建对象

/*
* @ 使用Object或对象字面量创建对象
* @ 最基本的创建对象的方法
*/
// 创建一个student对象, new一个Object
var student = new Object();
student.name = "easy";
student.age = "20";
// 如果嫌这种方法麻烦, 有种封装不良的感觉,试试字面量创建

对象字面量方式来创建对象

/*
* @ 对象字面量方式来创建对象
* @ 缺点,无法重复
*/
var student = {
    name: "easy",
    age: "20"
}

// 当我们要创建同类的student1, student2,...,studentN时, 不得不将以上的代码重复N次
var student2 = {
    name: "easy2",
    age: "20"
}
// ...
var studentn = {
    name: "easyn",
    age: "20"
}
// 这样麻烦而且重复太多, 能不能像工厂间那样, 有一个车床不断生产出对象.

工厂模式创建对象

/*
* @ 工厂模式创建对象
* @ JS中没有类的概念, 那么我们不妨就使用一种函数将以上对象创建过程封装起来以便于重复调用
* @ 同时可以给出特定接口来初始化对象
*/
function createStudent(name, age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    return obj;
}

var student1 = createStudent("easy1", 20);
var student2 = createStudent("easy2", 20);
// ...
var studentn = createStudent("easyn", 20);

// 这样就可以通过createStudent函数源源不断地"生产"对象了, 看起来很完美了,但我们不仅希望"产品"的生产可以像工厂间一样源源不断,还想知道生产的产品究竟是哪一种类型.

// 这样就可以通过createStudent函数源源不断地"生产"对象了, 看起来很完美了,但我们不仅希望"产品"的生产可以像工厂间一样源源不断,还想知道生产的产品究竟是哪一种类型

// 我们希望v1是student类型, 而v2是fruit类型, 那我们可以用构造函数来创建对象

/*
* @ "构造函数" 和 "普通函数" 有什么区别
* @ 一.实际上并不存在创建构造函数的特殊语法, 构造函数就是一个普通函数, 与普通函数唯一的区别在于调用方法
* @ 二.对于任意函数,使用new操作符调用, 那么它就是构造函数; 不使用new操作符调用, 那么它就是普通函数
* @ 三.按照惯例,我们约定构造函数名以大写字母开头
* @ 四.使用new操作符调用构造函数时,会经历4个阶段
*   @ 1.创建一个新对象
*   @ 2.将构造函数作用赋给新对象(使用tihs指向该新对象)
*   @ 3.执行构造函数代码
*   @ 4.返回新对象
*/

构造函数模式创建对象

// 将工厂模式函数重写, 并添加一个方法属性
function Student(name, age){
    this.name = name;
    this.age = age;
    this.alertName = function(){
        alert(this.name);
    }
}

function Fruit(name, color){
    this.name = name;
    this.color = color;
    this.alertName = function(){
        alert(this.name);
    }
}

// 分别new出Student和Fruit的对象
var v1 = new Student("easy", 20);
var v2 = new Fruit("apple", "green");

// 这样就可以用instanceof操作符来检测以上对象类型区分出不同的对象
console.log(v1 instanceof Student);        // true
console.log(v2 instanceof Student);        // false
console.log(v1 instanceof Fruit);          // false
console.log(v2 instanceof Fruit);          // true
console.log(v1 instanceof Object);         // true
console.log(v2 instanceof Object);         // true

// 这样解决了工厂模式无法区分对象类型, 但这样就完美了吗, 在JS中,函数是对象, 当我们实例化不只一个Strudent对象的时候.

var v1 = new Student("easy1", 20);
var v2 = new Student("easy2", 20);
//...
var vn = new Student("easyn", 20);
// 其中共同的alertName()函数也被实例化了n次, 我们可以用以下方法来检测不同的Strudent对象并不共用alertName()函数
console.log(v1.alertName == v2.alertName);      // false

// 这无疑是一种内存的浪费,我们知道this对象是在运行时基于函数的执行环境进行绑定. 在全局函数中,this对象等同于window, 在对象方法中,this指向该对象.

// 试将对象方法移到构造函数外部
function Student(name, age){
    this.name = name;
    this.age = age;
    this.alertName = alertName;
}
function alertName(){
    alert(this.name);
}
var stu1 = new Student("easy1", 20);
var stu2 = new Student("easy2", 20);

// 在调用"stu1.alert()时", this对象才被绑定到stu1上, 通过alertName()函数定义为全局函数, 这样对象中的alertName属性则被设置为指向该全局的指针, 由此stu1和stu2共享了该全局函数, 解决了内在浪费的问题

// 但是,通过全局函数的方式解决对象内部共享的问题, 终究不像一个好的解决方法, 更好的方案是通过原型对象模式来解决

原型模式创建对象

一、基本概念:
1,对象:属性和方法的集合,即变量和函数的封装,每个对象都有一个proto属性,指向这个对象的构造函数的原型对象;
2,构造器函数:用于创建对象的函数,通过new关键字生成对象。函数名一般首字母大写;
3,原型对象:每个函数都有一个prototype属性,它是一个指向原型对象的指针(原型对象在定义函数时同时被创建)

二、创建对象的方法
1,使用构造函数和原型对象共同创建:


原型对象photo1.png

如上图,构造器函数Person(),通过new关键字创建了两个实例化对象p1、p2,这两个新对象都继承了,构造器Person()函数prototype属性所指向的原型对象。通过构造函数创建实例对象p1和p2的时候,其中name、age、job这些是通过构造函数生成的(本地部分),sayName方法是通过继承原型对象来实现共享的(远程部分),这样多个实例对象都是由本地(私有)和远程(共享)两部分组成。如果还是不清楚,没关系我们上代码。

function Person(name, age, job){//构造器函数
    this.name = name;
    this.age = age;
    this.job = job;
}
Person.prototype = {//设置构造器函数prototype指定的对象,即重置原型对象
    constructor : Person,
    sayName : function(){alert(this.name);}
}
var p1 = new Person("Tom", 29, "Teacher");//实例化对象p1
//{name:"Tom",age:29,job:"Teacher",__proto__:object},object即原型对象:Person.prototype指向的对象
var p2 = new Person("Jack", 27, "Doctor");//实例化对象p2
//{name:"Jack",age:27,job:"Doctor",__proto__:object}

2、仅使用原型对象创建


原型对象photo2.png

如上图,使用Object.create方法从原型对象直接生成新的实例对象,新对象p1继承原型对象的属性和方法,但是这里没有用到构造函数
var person={ classname:'human'}//将这个对象当做原型
var p1=Object.create(person)//生成实例对象
console.log(p1.classname)//human,相当于p1.proto.classname
构造函数是妈,原型对象是爸,实例对象是孩子。妈让每个孩子拥有私有能力,爸让它们拥有共有能力(这个共有能力其实都是爸代劳的/(ㄒoㄒ)/~);没有构造函数的情况下,可以直接理解为克隆哦怎么样,这样应该能理解三者之间的关系了吧。

使用原型模型创建对象
直接在原型对象中添加属性和方法

function Student() {
}

Student.prototype.name = "easy";
Student.prototype.age = 20;
Student.prototype.alertName = function(){
                                alert(this.name);
                              };

var stu1 = new Student();
var stu2 = new Student();

stu1.alertName();  //easy
stu2.alertName();  //easy

alert(stu1.alertName == stu2.alertName);  //true 二者共享同一函数

以上代码,我们在Student的protptype对象中添加了name、age属性以及alertName()方法。但创建的stu1和stu2中并不包含name、age属性以及alertName()方法,而只包含一个[[prototype]]指针属性。当我们调用stu1.name或stu1.alertName()时,是如何找到对应的属性和方法的呢?

当我们需要读取对象的某个属性时,都会执行一次搜索。首先在该对象中查找该属性,若找到,返回该属性值;否则,到[[prototype]]指向的原型对象中继续查找。

由此我们也可以看出另外一层意思:如果对象实例中包含和原型对象中同名的属性或方法,则对象实例中的该同名属性或方法会屏蔽原型对象中的同名属性或方法。原因就是“首先在该对象中查找该属性,若找到,返回该属性值;”

拥有同名实例属性或方法的示意图:
在访问stu1.name是会得到”EasySir”:
alert(stu1.name); //EasySir

通过对象字面量重写原型对象
很多时候,我们为了书写的方便以及直观上的”封装性”,我们往往采用对象字面量直接重写整个原型对象:

function Student() {
}

Student.prototype = {
  constructor : Student,
  name : "easy",
  age : 20,
  alertName : function() {
    alert(this.name);
  }
};

要特别注意,我们这里相当于用对象字面量重新创建了一个Object对象,然后使Student的prototype指针指向该对象。该对象在创建的过程中,自动获得了新的constructor属性,该属性指向Object的构造函数。因此,我们在以上代码中,增加了constructor : Student使其重新指回Student构造函数。
原型模型创建对象的局限性

原型模型在对象实例共享数据方面给我们带来了很大的便利,但通常情况下不同的实例会希望拥有属于自己单独的属性。我们将构造函数模型和原型模型结合使用即可兼得数据共享和”不共享”。
构造与原型混合模式创建对象

我们结合原型模式在共享方法属性以及构造函数模式在实例方法属性方面的优势,使用以下的方法创建对象:
//我们希望每个stu拥有属于自己的name和age属性

function Student(name, age) {
  this.name = name;
  this.age = age;
}

//所有的stu应该共享一个alertName()方法
Student.prototype = {
  constructor : Student,
  alertName : function() {
                alert(this.name);
              }
}

var stu1 = new Student("Jim", 20);
var stu2 = new Student("Tom", 21);

stu1.alertName();  //Jim  实例属性
stu2.alertName();  //Tom  实例属性

alert(stu1.alertName == stu2.alertName);  //true  共享函数

以上,在构造函数中定义实例属性,在原型中定义共享属性的模式,是目前使用最广泛的方式。通常情况下,我们都会默认使用这种方式来定义引用类型变量。

通过对象字面量重写原型对象

function Student(){}

Student.prototype = {
    constructor: Student,
    name: "easy",
    age: 20,
    alertName: function(){
        alert(this.name);
    }
}
// 要特别注意,我们这里相当于用对象字面量重新创建了一个Object对象,然后使Student的prototype指针指向该对象。该对象在创建的过程中,自动获得了新的constructor属性,该属性指向Object的构造函数。因此,我们在以上代码中,增加了constructor : Student使其重新指回Student构造函数。

// 原型模型创建对象的局限性
// 原型模型在对象实例共享数据方面给我们带来了很大的便利, 但通常情况下不同的实例会希望拥有属性自己的单独的属性, 我们将构造函数的模型和原型模型结合使用即可兼得数据共享和"不共享"

构造与原型混合模式创建对象

/*
* 构造与原型混合模式创建对象
* 我们结合原型模式在共享方法属性以及构造函数模式在实例方法属性方面的优势,使用以下的方法创建对象
* 在构造函数中定义实例属性,在原型中定义共享属性的模式,是目前使用最广泛的方式。通常情况下,我们都会默认使用这种方式来定义引用类型变量。
*/

// 我们希望每个stu拥有属于自己的name和age属性
function Student(name, age){
    this.name = name;
    this.age = age;
}

// 所有的stu应该共享一个alertName()方法
Student.prototype = {
    constructor: Student,
    alertName: function(){
        alert(this.name);
    }
}

var stu1 = new Student("Jim", 20);
var stu2 = new Student("Tom", 21);

stu1.alertName();
stu2.alertName();

alert(stu1.alertName == stu2.alertName);  //true  共享函数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容