需求
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