首頁  >  文章  >  web前端  >  Javascript基礎回顧之(三) js面向對象

Javascript基礎回顧之(三) js面向對象

高洛峰
高洛峰原創
2017-02-04 14:56:17953瀏覽

本來是要繼續由淺入深表達式系列最後一篇的,但是最近團隊突然就忙起來了,從來沒有過的忙!不過喜歡表達式的朋友請放心,已經在寫了:) 在工作當中發現大家對Javascript的一些基本原理普遍存在這裡或者那裡的一知半解,所以決定先花一些時間整理一下這些基礎知識和大家分享。 後面會附上培訓用的PPT。剛開始打算寫一篇的,但後來寫著寫著就發現越來越多,所以決定還是寫一個系列吧。這個系列所有內容都是涉及Javascript基礎的,沒有時髦的玩意兒,但是我相信這些基礎的東西會有助於你理解那些有趣的東西的。

  本篇是你必須知道的Javascript系列第三篇,我們主要來看看Javascript是如何面向對象的程式設計的。主要涉及以下內容:

Javascript中的物件

什麼是物件

遍歷屬性

創建物件

工廠模式

構造函數

創建物件

工廠模式

構造函數在構造函數中

在call和apply中

在bind中

在dom元素事件處理函數中

詳解prototype

什麼是原型

鏈是原型繼承鏈是原型鏈鏈中的問題

Javascript中的物件

什麼是物件

我們可以把Javascript中物件理解為一組無序的鍵值對,就好像C#中的Dictionary一樣。 Key是屬性的名稱,而value可以是以下3種類型:

基本值(string, number, boolean, null, undefined)

物件

函數

var o = new Object();
o["name"] = "jesse"; //基本值作为对象属性
o["location"] = {   //对象作为对象属性
  "city": "Shanghai",
  "district":"minhang"
};
 
// 函数 作为对象属性
o["sayHello"] = function () {
  alert("Hello, I am "+ this.name + " from " + this.location.city);
}
 
o.sayHello();

㟎可以用foreach對Dictionary進行遍歷的,如果說物件在Javascript中是一組鍵值對的話,那我們要如何進行遍歷呢?

for (var p in o) {
  alert('name:'+ p +
     ' type:' + typeof o[p]
    );
}
// name:name type:string
// name:location type:object
// name:sayHello type:function

上面的這種遍歷方式會把原型中的屬性也包括進來,關於什麼是原型,以及如何區分原型和實例中的屬性我們下面會講到。

創建對象

  其實在上面我們已經創建了一個對象,並且使用了以下兩種創建對象的方式。

利用new建立一個Object的實例。

字面量

我們上面的o是用第一種方式創建的,而o中的location屬性則是用字面量的方式創建的。而第一種方式其實也有一個名字叫做建構子模式,因為Object其實就是一個建構函數,為我們產生了一個Object的實例。如果對於建構函式這一塊還有不清楚的話,趕快去看我的第一篇 型別基礎Object與object吧。

  除了以上兩種方式以外,我們一些創建對象的方式,我們也來一起看一下:

工廠模式

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('Jesse', 29, 'Software Engineer');
var person2 = createPerson('Carol', 27, 'Designer');

這種模式創建的對像有一個問題,那就是它在函數的內部為我建立了一個Object的實例,這個實例跟我們的建構子createPerson是沒有任何關係的。

因為我在內部用new Object()來創建了這個對象,所以它是Object的實例。所以如果我們想知道它是具體哪個function的實例,那就不可能了。

建構函數模式

  工廠模式沒有解決物件辨識的問題,但是我們可以想一下,Object()其實也是一個函數,只不過當我在它前面加上一個new的時候,它就變成了一個建構函式為我們產生一個Object的實例。那我同樣也可以在其它函數前面加上new這樣就可以產生這個函數的實例了,這就是所謂的建構子模式。

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function(){
    alert(this.name);
  };
}
 
var p1 = new Person('Jesse', 18, 'coder');
alert(p1 instanceof Person); // true

詳解thisJavascript基础回顾之(三) js面向对象  this在Javascript中也可以算是一個很神奇對象,沒錯this是一個對象。我們在上一篇作用域和作用域鏈中講到了變數對象,變數對象決定了在當前的執行環境中有哪些屬性和函數是可以被存取的,從某種程度上來說我們就可以把this看作是這個變數物件。我們之前提到了最大的執行環境是全域執行環境,而window就是全域執行環境中的變數對象,那麼我們在全域環境中this===window是會回傳true的。

除了全域執行環境以外,我們也提到了另一個執行環境,也就是函數。每一個函數都有一個this對象,但有時候他們所代表的值是不一樣的,主要是由這個函數的呼叫者來決定的。讓我們來看看以下幾個場景:

函數

function f1(){
 return this;
}
 
f1() === window; // global object

因为当前的函数在全局函数中运行,所以函数中的this对象指向了全局变量对象,也就是window。这种方式在严格模式下会返回undefined。

对象方法

var o = {
 prop: 37,
 f: function() {
  return this.prop;
 }
};
 
console.log(o.f()); // logs 37

在对象方法中,this对象指向了当前这个实例对象。注意: 不管这个函数在哪里什么时候或者怎么样定义,只要它是一个对象实例的方法,那么它的this都是指向这个对象实例的。

var o = { prop: 37 };
var prop = 15;
 
function independent() {
  return this.prop;
}
 
o.f = independent;
console.log(independent()); // logs 15
console.log(o.f()); // logs 37

区别:上面的函数independent如果直接执行,this是指向全局执行环境,那么this.prop是指向我们的全局变量prop的。但是如果将independent设为对象o的一个属性,那么independent中的this就指向了这个实例,同理this.prop就变成了对象o的prop属性。

构造函数

  我们上面讲到了用构造函数创建对象,其实是利用了this的这种特性。在构造函数中,this对象是指向这个构造函数实例化出来的对象。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    alert(this.name);
  };
}
 
var p1 = new Person('Jesse', 18, 'coder');
var p2 = new Person('Carol',16,'designer');

当我们实例化Person得到p1的时候,this指向p1。而当我们实例化Person得到p2的时候,this是指向p2的。

利用call和apply

  当我们用call和apply去调用某一个函数的时候,这个函数中的this对象会被绑定到我们指定的对象上。而call和apply的主要区别就是apply要求传入一个数组作为参数列表。

function add(c, d) {
  return this.a + this.b + c + d;
}
 
var o = { a: 1, b: 3 };
 
// 第一个参数会被绑定成函数add的this对象
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
 
// 第二个参数是数组作为arguments传入方法add
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

在bind方法中

  bind方法是 存在于function的原型中的 Function.prototype.bind,也就是说所有的function都会有这个方法。但我们调用某一个方法的bind的时候,会产生一个和原来那个方法一样的新方法,只不过this是指向我们传得bind的第一个参数。

function f() {
  return this.a;
}
 
var g = f.bind({ a: "azerty" });
console.log(g()); // azerty
 
var o = { a: 37, f: f, g: g };
console.log(o.f(), o.g()); // 37, azerty

在dom元素事件处理器中

  在事件处理函数中,我们的this是指向触发这个事件的dom元素的。

HTML代码

<html>
<body>
  <div id="mydiv" style="width:400px; height:400px; border:1px solid red;"></div>
  <script type="text/javascript" src="essence.js"></script>
</body>
</html>

JavaScript代码

function click(e) {
  alert(this.nodeName);
}
 
var myDiv = document.getElementById("mydiv");
myDiv.addEventListener(&#39;click&#39;, click, false);

当我们点击页面那个div的时候,毫无疑问,它是会显示DIV的。

Javascript基础回顾之(三) js面向对象

详解prototype
  prototype即原型,也是Javascrip中一个比较重要的概念。在说原型之前呢,我们需要回顾一下之前的构造函数模式。在我们用构造函数去创建对象的时候主要是利用了this的特性。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    alert(this.name);
  };
}
 
var p1 = new Person(&#39;Jesse&#39;, 18, &#39;coder&#39;);
var p2 = new Person(&#39;Carol&#39;, 17, &#39;designer&#39;);

我们上面还讲到了当用Person实例化p1的时候Person中的this是指向p1的,当实例化p2的时候呢,this是指向p2的。那也就是说,p1和p2中的sayName虽然起到了同样的作用,但是实际上他们并非是一个函数。

Javascript基础回顾之(三) js面向对象

也就是说他们内存堆中是存在多份拷贝的,而不是在栈中引用地址的拷贝。先不说这符不符合面向对象的思想,至少这对于内存来说也是一种浪费。而解决办法就是我们要讨论的原型。

什么是原型

  在Javascript中的每一个函数,都会有一个原型对象,这个原型对象和我们普通的对象没有区别。只不过默认会有一个constructor属性指向这个函数。 同时,所有这个函数的实例都会有一个引用指向这个原型对象。如果不太清楚,那就看看下面这张图吧:

Javascript基础回顾之(三) js面向对象

以上就是构造函数,构造函数原型,以及实例之间的关系。以我们的Person构造函数为例,所有Person的实例(p1,p2)都舒服一个prototype属性指向了Person构造函数prototype对象。如此一来,我们就可以把方法写在原型上,那么我们所有的实例就会访问同一个方法了。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
 
  Person.prototype.sayName = function () {
    alert(this.name);
  }
}
var p1 = new Person(&#39;Jesse&#39;, 18, &#39;coder&#39;);
var p2 = new Person(&#39;Carol&#39;, 17, &#39;designer&#39;);
 
alert(p1.sayName == p2.sayName); // true

   

什么是原型链

  大家还记得作用域链么?如果不记得,请自觉到第二篇中去复习(作用域和作用域链)。简单的来说,我们在一个执行环境中访问某个变量的时候如果当前这个执行环境中不存在这个变量,那么会到这个执行环境的包含环境也就是它的外层去找这个变量,外层还找不到那就再外一层,一直找到全局执行环境为止,这就是作用域链。而原型链有点类型,只不过场景换到了我们的对象实例中,如果我在一个实例中找某一个属性,这个实例中没有,那就会到它的原型中去找。记住,我们上面说了,原型也是一个对象,它也有自己的原型对象,所以就行成了一个链,实例自己的原型中找不到,那就到原型的原型对象中去找,一直向上延伸到Object的原型对象,默认我们创建的函数的原型对象它自己的原型对象是指向Object的原型对象的,所以这就是为什么我们可以在我们的自定义构造函数的实例上调用Object的方法(toString, valueOf)。

Javascript基础回顾之(三) js面向对象

利用原型实现继承

  其实我们上面已经讲了继承在Javascript中的实现,主要就是依靠原型链来实现的。所有的实例是继承自object就是因为在默认情况下,我们所有创建函数的原型对象的原型都指向了object对象。同理,我们可以定义自己的继承关系。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayName = function () {
  alert(this.name);
}
 
function Coder(language){
  this.language = language;
}
Coder.prototype = new Person(); //将 Coder 的原型指向一个Person实例实现继Person
Coder.prototype.code = function () {
  alert(&#39;I am a &#39;+ this.language +&#39; developer, Hello World!&#39;);
}
 
function Designer() {
}
Designer.prototype = new Person(); //将 Desiger 的原型指向一个Person实例实现继Person
Designer.prototype.design = function () {
  alert(&#39;其实我只是一个抠图工而已。。。。&#39;);
}
 
var coder = new Coder(&#39;C#&#39;);
coder.name = &#39;Jesse&#39;;
coder.sayName(); //Jesse
coder.code();   // I am a C# developer, Hello World!
 
var designer = new Designer();
designer.name = &#39;Carol&#39;;
designer.sayName(); // Carol
designer.design();  // 其实我只是一个抠图工而已。。。。

   

原型链中的问题

  由于原型对象是以引用的方式保存的,所以我们在赋值的时候要特别注意,一不小心就有可能把之前赋的值给赋盖了。比如上面的代码中,我们先写原型方法,再实现继承,那我们的原型方法就没有了。

function Coder(language){
  this.language = language;
}
Coder.prototype.code = function () {
  alert(&#39;I am a &#39;+ this.language +&#39; developer, Hello World!&#39;);
}
Coder.prototype = new Person(); //这里会覆盖上面所有的原型属性和方法
var coder = new Coder(&#39;C#&#39;);
coder.name = &#39;Jesse&#39;;
coder.sayName();
coder.code();   // 这里会报错,找不到code方法。

这样三篇文章都完成了

更多Javascript基础回顾之(三) js面向对象相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn