需求
Javascript是一種基於對象(object-based)的語言,你遇到的所有東西幾乎都是對象。
比如下面這是一條貪喫蛇(你自己想象);我們如何創建並讓它移動
如果我想讓它移動:基本的思路是每隔段時間就創建一條貪喫蛇;並刪除之前創建的刪除蛇
生成實例對象的原始模式-對象字面量來創建這條蛇
假定我們把蛇看成一個對象,它有每一節的寬度、高度;還有身體;還有標識的name屬性(重新創建的蛇不再是之前那條蛇了)
var snake1 = {
name:'a',
width: 20,//寬度
height: 20,//高度
body:[
{x:3,y:1,color:'red'},//頭部的x座標;y座標;顏色
{x:2,y:1,color:'blue'},
{x:1,y:1,color:'blue'}
]
}
我想讓蛇這個對象前進一步
var snake2 = {
name:'b',
width: 20,//寬度
height: 20,//高度
body:[
{x:4,y:1,color:'red'},//頭部的x座標;y座標;顏色
{x:3,y:1,color:'blue'},
{x:2,y:1,color:'blue'}
]
}
如果想讓其移動很多步;或則我想讓頭部顏色不一樣;或則我要給其一個name屬性不同值來標識…這樣每次都要創建一個對象,以上會創建大量的代碼,寫起來會非常麻煩
使用new Object()創建對象
和上面是一樣的
工廠函數
我們可以寫一個函數;解決代碼重複的問題
雖然解決了集中使用實例對象,但是重複創建不同對象的的屬相和方法,消耗瀏覽器的內存
function snake(name,x){
return {
name:name,
width: 20,//寬度
height: 20,//高度
body:[
{x:x+3,y:1,color:'red'},
{x:x+2,y:1,color:'blue'},
{x:x+1,y:1,color:'blue'}
]
}
}
var = snake(a,0)
var snake2 = snake(b,1)
這種方法的問題依然是,snake1和snake2之間沒有內在的聯繫,不能反映出它們是同一個原型對象的實例。
構造函數模型
構造函數 ,是一種特殊的函數。主要用來在創建對象時初始化對象, 即爲對象成員變量賦初始值,總與new運算符一起使用在創建對象的語句中。
function Snake(name,x){
this.w = 20
this.h = 20
this.name = name
this.body = [
{x:x+3,y:1,color:'red'},
{x:x+2,y:1,color:'blue'},
{x:x+1,y:1,color:'blue'}
]
}
var snake1 = new Snake(a,0)
var snake2 = new Snake(b,1)
new的作用
- new會在內存中創建一個新的空對象
- new 會讓this指向這個新的對象
- 執行構造函數 目的:給這個新對象加屬性和方法
- new會返回這個新對象
js不是一門面向對象的語言
面向對象是一個類的概念
- js只是面向過程的語言;面向過程也就是如何解決==》找到解決問題的方法(函數)
2.面向對象是一種思想,使用對象解決問題創建一個對象,讓對象擁有做某件事的能力(也就是給對象一種屬性和方法),然後命令做某件事(封裝、繼承、多態).誰能解決==》找到解決問題的對象
私有屬性和私有方法
只要不是定義在構造函數t對象上的方法和屬性,都是私有的。
function Hero(name, blood){
var name = name;
}
靜態成員和實例成員
- 實例成員 / 對象成員 : 跟對象相關的成員,將來使用對象的方式來調用;構造函數this上的成員都是實例
- 靜態成員:直接給構造函數添加的成員;
function Hero(name, blood){
this.name = name;
}
Hero.version = '1.0'
var hero = new Hero('xwh')
console.log(hero.name) //實例對象調用靜態成員
console.log(hero.version) //靜態成員不能使用對象的方式來調用;打印undefined
console.log(Hero.version);// 靜態成員使用構造函數來調用
constructor和原型
爲了解決從原型對象生成實例的問題,Javascript提供了一個構造函數(Constructor)模式。
也就是說上面的hero自動含有一個constructor屬性,指向它們的構造函數。
console.log(hero.constructor===Hero) //true
Javascript規定,每一個構造函數都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。
再來看這段代碼
function Hero(name, blood){
this.name = name;
this.attack = function () {
console.log(' 攻擊敵人');
}
}
對於每一個實例對象,每一次生成一個實例調用attack方法,都必須爲重複的內容,多佔用一些內存。這樣既不環保,也缺乏效率。
var hero1 = new Hero('xwh')
var hero2 = new Hero('hwx')
console.log(hero.attack == hero1.attack) //false
Javascript規定,每一個構造函數都有一個prototype原型屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。
Hero.prototype.attack = function(){
//原型是js爲函數創建的一個屬性,所以每一個函數都有一個原型/原型對象
}
這意味着,我們可以把那些不變的屬性和方法,直接定義在prototype對象上。這時所有實例屬性和方法,其實都是同一個內存地址,指向prototype對象,因此就提高了運行效率。
- 當對象調用屬性和方法,首先會去找對象本身的屬性和方法(this.attack)
- 如果沒有則會去找原型prototype的屬性和方法
原型鏈
可能畫起來看的亂亂的;沒事;看代碼
function Back(name,age){
this.name = name
this.age = age
}
var back = new Back('hh',18)
console.log(back)
打印結果可以看到有一個__proto__
屬性;
它是指向構造函數的原型
console.log(back.__proto__===Back.prototype) //true
那麼我們在繼續查看
console.log(back.__proto__.__proto__)//指向了Object的原型
最後Object的原型指向了null
console.log(back.__proto__.__proto__.__proto__)//null
原型鏈:由實例對象的
__proto__
屬性和對象的構造函數的原型的__proto__
構成的鏈式結構
原型鏈的頂端是Object
,object的__proto__
指向了自己(null
)
通過上面可知;以下三者都是指向構造函數的本身的
console.log(back.constructor)
console.log(back.__proto__.constructor)
console.log(Back.prototype.constructor)
繼承
繼承的本質
繼承實質上是複製父類,並不是真正的繼承。繼承的目的是爲代碼重用
var wjl = {
name: 'wjl',
money: 10000000,
cars: ['瑪莎拉蒂', '特斯拉'],
houses: ['別墅', '大別墅'],
play: function () {
console.log('打高爾夫');
}
}
var wxz = {
name: 'wxz'
}
for(var key in wjl){
if (wxz[key]) {
continue;
}
wxz[key] = wjl[key]
}
console.log(wxz)
prototype模式原型繼承–改變原型的指向
- 重新改變原型對象的prototype屬性,設置爲一個新的對象
- 從上面可以知道;原型的構造器指向構造函數本身;將其指向子類自己
function Wjl(){
this.money = 'money'
this.houses = ['別墅', '大別墅']
this.cars = '瑪莎拉蒂'
}
function Wsc(){
}
Wsc.prototype = new Wjl()
Wsc.prototype.constructor = Wsc
var wsc = new Wsc()
console.log(wsc) //Wsc {}
console.log(wsc.constructor);//實例自動生成的構造器還是指向了它自己的構造函數:Wsc
console.log(wsc.money); //money
- 實例對象的
__proto__
指向構造函數的原型prototype
(true);但是現在原型改變了;所以實例對象的__proto__
指向了父類構造函數Wjl
借用構造函數實現繼承–利用call或則apply
call或則apply都可以改變this的指向;只是他們的傳參不同
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
Person.prototype.sayHi = function () {
console.log(this.name);
}
function Student(name, age, sex, score){
Person.call(this, name, age, sex)
this.score = score;
}
var s1 = new Student('zs', 18, '男', 100);
console.dir(s1);
console.log(s1.__proto__)
console.log(s1.sayHi) //獲取不到;因爲它的的構造器指向的是Student
組合繼承
就是以上兩種方法的結合
- 如果單純的原型繼承;我們改變了原型的指向;可以繼承父類原型上的方法或屬性;但是卻無法傳參數
function Wjl(money){
this.money = money
}
- 如果單純的使用借用構造函數;解決了傳參問題;那父類原型上的方法和屬性又無法繼承
function Teacher(skill){
this.skill = skill
}
Teacher.prototype.say = function(){
console.log('...')
}
function Student(skill){
Teacher.call(this,skill)
}
// 原型繼承
Student.prototype = new Teacher()
Student.prototype.constructor = Student
var stu = new Student('math')
stu.say() // ...
console.log(stu.skill) //math
面向對象實現貪喫蛇的移動實戰
html結構
<div class="map" style="position: relative"></div>
在構造函數初始化一些必要的實例成員
function Snake(w,h){
this.w = w || 20 //寬度
this.h = h || 20 //高度
this.body = [
{x:3,y:1,color:'red'},
{x:2,y:1,color:'blue'},
{x:1,y:1,color:'blue'}
]
this.map = document.querySelector('.map')
}
創建一條蛇
Snake.prototype.create = function(){
this.body.forEach(item=>{ //根據初始的量將每個div
var box = document.createElement('div') //創建一個div
box.style.width = this.w + 'px'
box.style.height = this.h + 'px'
box.style.background = item.color
box.style.position = 'absolute'
box.style.left = this.w * item.x + 'px'
box.style.right = this.h * item.y + 'px'
this.map.appendChild(box)
})
}
測試一些
var snake = new Snake()
snake.create()
讓蛇動起來
Snake.prototype.move = function(){
setInterval(function(){
this.body = this.body.map(item=>{
item.x ++
return item
})
this.create()
}.bind(this), 1000)
}
測試
var snake = new Snake()
snake.create()
snake.move()
當然如果想要讓它看起來有移動效果創建之前先刪除
Snake.prototype.remove = function(){
console.log(this.map.children)
if(this.map.children.length){
for(var i = 0;i<this.map.children.length;i++){
this.map.removeChild(this.map.children[i])
}
}
}
Snake.prototype.create = function(){
this.remove()
this.remove()
......
}
比如限制它的範圍;如何根據鍵盤操控;可以思考一下;當然如果蛇如果要生一個小蛇;這個要使用組合繼承了
function SnakeSon(w,h){
Snake.call(this,w,h)//繼承Snake的成員
}
SnakeSon.prototype = new Snake()
SnakeSon.prototype.constructor = SnakeSon
var snakeSon = new SnakeSon()
snakeSon.move()
我們如果想要創造一個食物
function Food(w,h){
Snake.call(this,w,h)//繼承Snake的成員
}
Food.prototype = new Snake()
Food.prototype.constructor = Food
Food.prototype.createRect = function(){
var box = document.createElement('div') //創建一個div
box.style.width = this.w + 'px'
box.style.height = this.h + 'px'
box.style.background = 'red'
box.style.position = 'absolute'
box.style.left = this.w * 5 + 'px'//可以定義個隨機數
box.style.right = this.h * 5 + 'px'
this.map.appendChild(box)
}
var food = new Food()
food.createRect()
當然這樣創建不是很好;因爲我們應該改Snake.prototype.create
裏面的代碼改造一下;讓食物也可以繼承;當然這裏只是爲了說明
class
構造函數和class定義的類的區別是有無狀態;這也決定了他們的用途
類體和方法定義
類聲明和類表達式的主體都執行在嚴格模式下。比如,構造函數,靜態方法,原型方法,getter和setter都在嚴格模式下執行。
構造函數
constructor方法是一個特殊的方法,這種方法用於創建和初始化一個由class創建的對象。一個類只能擁有一個名爲 “constructor”的特殊方法。如果類包含多個constructor的方法,則將拋出 一個SyntaxError 。
原型方法
class Person{
constructor(name,age){
this.name = name
this.age = age
}
// 狀態
get skill(){
return 'teaches'
// return this.say()
}
// 方法
say(){
return this.name + ':' + this.age
}
}
var person = new Person('teacher','math')
console.log(person.skill) // teaches
console.log(person.say()) // teacher:math
靜態方法
static 關鍵字用來定義一個類的一個靜態方法。調用靜態方法不需要實例化該類,但不能通過一個類實例調用靜態方法。靜態方法通常用於爲一個應用程序創建工具函數。
一個類的類體是一對花括號/大括號 {} 中的部分。這是你定義類成員的位置,如方法或構造函數。
var person = new Person('teacher','math')
console.log(person.skill) // teaches
console.log(person.say()) // teacher:math
// static不能給實例對象調用
class Father {
static say(){
console.log('調用靜態方法不需要實例化該類,但不能通過一個類實例調用靜態方法。')
}
}
var father = new Father()
Father.say() //這個跟構造函數的實例成員和靜態成員是一樣;都是不能被實例對象所訪問
// father.say() 報錯
繼承
extends 關鍵字在類聲明或類表達式中用於創建一個類作爲另一個類的一個子類。
參考文檔
Javascript 面向對象編程(一):封裝
Javascript面向對象編程(二):構造函數的繼承
類- JavaScript | MDN