• 理解面向对象开发思想
• 掌握 JavaScript 面向对象开发相关模式
• 掌握在 JavaScript 中使用正则表达式
• 贪吃蛇
由于 JavaScript 高级还是针对 JavaScript 语言本身的一个进阶学习,所以在开始之前我们先对以前所学过的 JavaScript 相关知识点做一个快速复习总结。
• 解析执行:轻量级解释型的
• 语言特点:动态,头等函数 (First-class Function)
– 又称函数是 JavaScript 中的一等公民
• 执行环境:在宿主环境(host environment)下运行,浏览器是最常见的 JavaScript 宿主环境
– 但是在很多非浏览器环境中也使用 JavaScript ,例如 node.js
MDN-JavaScript
• ECMAScript - 语法规范
– 变量、数据类型、类型转换、操作符
– 流程控制语句:判断、循环语句
– 数组、函数、作用域、预解析
– 对象、属性、方法、简单类型和复杂类型的区别
– 内置对象:Math、Date、,基本包装类型String、Number、Boolean
• Web APIs
– BOM
• onload页面加载事件,window顶级对象
• 定时器
• location、history
– DOM
• 获取页面元素,注册事件
• 属性操作,样式操作
• 节点属性,节点层级
• 动态创建元素
• 事件:注册事件的方式、事件的三个阶段、事件对象
阿特伍德定律:
Any application that can be written in JavaScript, will eventually be written in JavaScript.
任何可以用JavaScript来写的应用,最终都将用JavaScript来写
阿特伍德 stackoverflow的创始人之一
• 知乎 - JavaScript 能做什么,该做什么?
• 最流行的编程语言 JavaScript 能做什么?
参考链接
User Interface 用户界面,我们所看到的浏览器Browser engine 浏览器引擎,用来查询和操作渲染引擎*Rendering engine 用来显示请求的内容,负责解析HTML、CSS,并把解析的内容显示出来Networking 网络,负责发送网络请求*JavaScript Interpreter(解析者) JavaScript解析器,负责执行JavaScript的代码UI Backend UI后端,用来绘制类似组合框和弹出窗口Data Persistence(持久化) 数据持久化,数据存储 cookie、HTML5中的sessionStorage
JavaScript 运行分为两个阶段:
• 预解析
– 全局预解析(所有变量和函数声明都会提前;同名的函数和变量函数的优先级高)
– 函数内部预解析(所有的变量、函数和形参都会参与预解析)
• 函数
• 形参
• 普通变量
• 执行
先预解析全局作用域,然后执行全局作用域中的代码,在执行全局代码的过程中遇到函数调用就会先进行函数预解析,然后再执行函数内代码。
Everything is object (万物皆对象)
对象到底是什么,我们可以从两次层次来理解。
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
• 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
• 面向对象就是找一个对象,指挥得结果
• 面向对象将执行者转变成指挥者
• 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性:
• 封装性
• 继承性
• [多态性]抽象
扩展阅读:
• 维基百科 - 面向对象程序设计
• 知乎:如何用一句话说明什么是面向对象思想?
• 知乎:什么是面向对象编程思想?
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
var std1 ={name:'Michael',score:98}var std2 ={name:'Bob',score:81}
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
functionprintScore (student) {console.log('姓名:'+student.name+' '+'成绩:'+student.score)}
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个printScore消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
functionStudent(name, score) {this.name= name;this.score= score;this.printScore=function() {console.log('姓名:'+this.name+' '+'成绩:'+this.score);}}
根据模板创建具体实例对象(Instance):
var std1 =newStudent('Michael',98)var std2 =newStudent('Bob',81)
实例对象具有自己的具体行为(给对象发消息):
std1.printScore() // =>姓名:Michael 成绩:98std2.printScore() // =>姓名:Bob 成绩 81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。
所以,面向对象的设计思想是:
• 抽象出 Class(构造函数)
• 根据 Class(构造函数) 创建 Instance
• 指挥 Instance 得结果
面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
我们可以直接通过new Object()创建:
var person =newObject()person.name='Jack'person.age=18person.sayName=function () {console.log(this.name)}
每次创建通过new Object()比较麻烦,所以可以通过它的简写形式对象字面量来创建:
var person ={name:'Jack',age:18,sayName:function () {console.log(this.name)}}
对于上面的写法固然没有问题,但是假如我们要生成两个person实例对象呢?
var person1 ={name:'Jack',age:18,sayName:function () {console.log(this.name)}}var person2 ={name:'Mike',age:16,sayName:function () {console.log(this.name)}}
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
我们可以写一个函数,解决代码重复问题:
functioncreatePerson (name, age) {return{name: name,age: age,sayName:function () {console.log(this.name)}}}
然后生成实例对象:
var p1 =createPerson('Jack',18)var p2 =createPerson('Mike',18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
内容引导:
• 构造函数语法
• 分析构造函数
• 构造函数和实例对象的关系
– 实例的 constructor 属性
– instanceof 操作符
• 普通函数调用和构造函数调用的区别
• 构造函数的返回值
• 构造函数的问题
一种更优雅的工厂函数就是下面这样,构造函数:
functionPerson (name, age) {this.name= namethis.age= agethis.sayName=function () {console.log(this.name)}}var p1 =newPerson('Jack',18)p1.sayName() // => Jackvar p2 =newPerson('Mike',23)p2.sayName() // => Mike
在上面的示例中,Person()函数取代了createPerson()函数,但是实现效果是一样的。这是为什么呢?
我们注意到,Person()中的代码与createPerson()有以下几点不同之处:
• 没有显示的创建对象
• 直接将属性和方法赋给了this对象
• 没有return语句
• 函数名使用的是大写的Person
而要创建Person实例,则必须使用new操作符。以这种方式调用构造函数会经历以下 4 个步骤:
1. 创建一个新对象
2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
3. 执行构造函数中的代码
4. 返回新对象
下面是具体的伪代码:
functionPerson (name, age) {// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象// var instance = {}// 然后让内部的 this 指向 instance 对象// this = instance// 接下来所有针对 this 的操作实际上操作的就是 instancethis.name= namethis.age= agethis.sayName=function () {console.log(this.name)}// 在函数的结尾处会将 this 返回,也就是 instance// return this}
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。在每一个实例对象中同时有一个constructor属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor=== Person) // => trueconsole.log(p2.constructor=== Person) // => trueconsole.log(p1.constructor===p2.constructor) // => true
对象的constructor属性最初是用来标识对象类型的,但是,如果要检测对象的类型,还是使用instanceof操作符更可靠一些:
console.log(p1 instanceofPerson) // => trueconsole.log(p2 instanceof Person) // => true
总结:
• 构造函数是根据具体的事物抽象出来的抽象模板
• 实例对象是根据抽象的构造函数模板得到的具体实例对象
• 每一个实例对象都具有一个constructor属性,指向创建该实例的构造函数
– 注意:constructor是实例的属性的说法不严谨,具体后面的原型会讲到
• 可以通过实例的constructor属性判断实例和构造函数之间的关系
– 注意:这种方式不严谨,推荐使用instanceof操作符,后面学原型会解释为什么
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
functionPerson (name, age) {this.name= namethis.age= agethis.type='human'this.sayHello=function () {console.log('hello '+this.name)}}var p1 =newPerson('Tom',18)var p2 =newPerson('Jack',16)
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type和sayHello都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello===p2.sayHello) // => false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
functionsayHello=function () {console.log('hello '+this.name)}functionPerson (name, age) {this.name= namethis.age= agethis.type='human'this.sayHello= sayHello}var p1 =newPerson('Top',18)var p2 =newPerson('Jack',16)console.log(p1.sayHello===p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
varfns={sayHello:function () {console.log('hello '+this.name)},sayAge:function () {console.log(this.age)}}functionPerson (name, age) {this.name= namethis.age= agethis.type='human'this.sayHello=fns.sayHellothis.sayAge=fns.sayAge}var p1 =newPerson('lpz',18)var p2 =newPerson('Jack',16)console.log(p1.sayHello===p2.sayHello) // => trueconsole.log(p1.sayAge===p2.sayAge) // => true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
• 构造函数语法
• 分析构造函数
• 构造函数和实例对象的关系
– 实例的 constructor 属性
– instanceof 操作符
• 构造函数的问题
内容引导:
• 使用 prototype 原型对象解决构造函数的问题
• 分析构造函数、prototype 原型对象、实例对象三者之间的关系
• 属性成员搜索原则:原型链
• 实例对象读写原型对象中的成员
• 原型对象的简写形式
• 原生对象的原型
– Object
–
– String
– ...
• 原型对象的问题
• 构造的函数和原型对象使用建议
JavaScript 规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的所拥有。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在prototype对象上。
functionPerson (name, age) {this.name= namethis.age= age}console.log(Person.prototype)Person.prototype.type='human'Person.prototype.sayName=function () {console.log(this.name)}varp1 =newPerson(...)var p2 =newPerson(...)console.log(p1.sayName===p2.sayName) // => true
这时所有实例的type属性和sayName()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
任何函数都具有一个prototype属性,该属性是一个对象。
functionF () {}console.log(F.prototype) // => objectF.prototype.sayHi=function () {console.log('hi!')}
构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在函数。
console.log(F.prototype.constructor=== F) // => true
通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype对象的指针__proto__。
varinstance =newF()console.log(instance.__proto__===F.prototype) // => true
__proto__是非标准属性。
实例对象可以直接访问原型对象成员。
instance.sayHi() // => hi!
总结:
• 任何函数都具有一个prototype属性,该属性是一个对象
• 构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在函数
• 通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype对象的指针__proto__
• 所有实例都直接或间接继承了原型对象的成员
了解了构造函数-实例-原型对象三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
• 搜索首先从对象实例本身开始
• 如果在实例中找到了具有给定名字的属性,则返回该属性的值
• 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
• 如果在原型对象中找到了这个属性,则返回该属性的值
也就是说,在我们调用person1.sayName()的时候,会先后执行两次搜索:
• 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
• ”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
• ”于是,它就读取那个保存在原型对象中的函数。
• 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
• 先在自己身上找,找到即返回
• 自己身上找不到,则沿着原型链向上查找,找到即返回
• 如果一直到原型链的末端还没有找到,则返回undefined
读取:
• 先在自己身上找,找到即返回
• 自己身上找不到,则沿着原型链向上查找,找到即返回
• 如果一直到原型链的末端还没有找到,则返回undefined
值类型成员写入(实例对象.值类型成员 = xx):
• 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
• 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx):
• 同上
复杂类型修改(实例对象.成员.xx = xx):
• 同样会先在自己身上找该成员,如果自己身上找到则直接修改
• 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
• 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
我们注意到,前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
functionPerson (name, age) {this.name= namethis.age= age}Person.prototype={type:'human',sayHello:function () {console.log('我叫'+this.name+',我今年'+this.age+'岁了')}}
在该示例中,我们将Person.prototype重置到了一个新的对象。这样做的好处就是为Person.prototype添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了constructor成员。
所以,我们为了保持constructor的指向正确,建议的写法是:
functionPerson (name, age) {this.name= namethis.age= age}Person.prototype={constructor: Person,// =>手动将 constructor 指向正确的构造函数type:'human',sayHello:function () {console.log('我叫'+this.name+',我今年'+this.age+'岁了')}}
所有函数都有 prototype 属性对象。
• Object.prototype
• Function.prototype
• .prototype
• String.prototype
• Number.prototype
• Date.prototype
• ...
练习:为数组对象和字符串对象扩展原型方法。
• 私有成员(一般就是非函数成员)放到构造函数中
• 共享成员(一般就是函数)放到原型对象中
• 如果重置了prototype记得修正constructor的指向
演示:贪吃蛇
游戏的目的是用来体会js高级语法的使用不需要具备抽象对象的能力,使用面向对象的方式分析问题,需要一个漫长的过程。
放一个容器盛放游戏场景div#map,设置样式
#map{width:800px;height:600px;background-color:#ccc;position:relative;}
• 游戏对象
• 蛇对象
• 食物对象
• Food
– 属性
• x
• y
• width
• height
• color
– 方法
• render 随机创建一个食物对象,并输出到map上
• 创建Food的构造函数,并设置属性
var position ='absolute';var elements = [];functionFood(x, y, width, height, color) {this.x= x ||0;this.y= y ||0;// 食物的宽度和高度(像素)this.width= width ||20;this.height= height ||20;// 食物的颜色this.color= color ||'green';}
• 通过原型设置render方法,实现随机产生食物对象,并渲染到map上
Food.prototype.render=function (map) {// 随机食物的位置,map.宽度/food.宽度,总共有多少分food的宽度,随机一下。然后再乘以food的宽度this.x=parseInt(Math.random() *map.offsetWidth / this.width) *this.width;this.y=parseInt(Math.random() *map.offsetHeight / this.height) *this.height;// 动态创建食物对应的divvar div =document.createElement('div');map.appendChild(div);div.style.position= position;div.style.left=this.x+'px';div.style.top=this.y+'px';div.style.width=this.width+'px';div.style.height=this.height+'px';div.style.backgroundColor=this.color;elements.push(div);}
• 通过自调用函数,进行封装,通过window暴露Food对象
window.Food= Food;
• Snake
• 属性
– width 蛇节的宽度默认20
– height 蛇节的高度默认20
– body 数组,蛇的头部和身体,第一个位置是蛇头
– direction 蛇运动的方向默认right可以是 left top bottom
• 方法
– render 把蛇渲染到map上
• Snake构造函数
var position ='absolute';var elements = [];functionSnake(width, height, direction) {// 设置每一个蛇节的宽度this.width= width ||20;this.height= height ||20;// 蛇的每一部分, 第一部分是蛇头this.body= [{x:3,y:2,color:'red'},{x:2,y:2,color:'red'},{x:1,y:2,color:'red'} ];this.direction= direction ||'right';}
• render方法
Snake.prototype.render=function(map) {for(var i =0; i <this.body.length; i++) {var obj =this.body[i];var div =document.createElement('div');map.appendChild(div);div.style.left=obj.x*this.width+'px';div.style.top=obj.y*this.height+'px';div.style.position= position;div.style.backgroundColor=obj.color;div.style.width=this.width+'px';div.style.height=this.height+'px';}}
• 在自调用函数中暴露Snake对象
window.Snake= Snake;
游戏对象,用来管理游戏中的所有对象和开始游戏
• Game
– 属性
• food
• snake
• map
– 方法
• start 开始游戏(绘制所有游戏对象)
• 构造函数
functionGame(map) {this.food=newFood();this.snake=newSnake();this.map= map;}
• 开始游戏,渲染食物对象和蛇对象
Game.prototype.start=function () {this.food.render(this.map);this.snake.render(this.map);}
• 在蛇对象(snake.js)中,在Snake的原型上新增move方法
5. 让蛇移动起来,把蛇身体的每一部分往前移动一下
6. 蛇头部分根据不同的方向决定往哪里移动
Snake.prototype.move=function (food, map) {// 让蛇身体的每一部分往前移动一下var i =this.body.length-1;for(; i >0; i--) {this.body[i].x=this.body[i -1].x;this.body[i].y=this.body[i -1].y;}// 根据移动的方向,决定蛇头如何处理switch(this.direction) {case'left':this.body[0].x-=1;break;case'right':this.body[0].x+=1;break;case'top':this.body[0].y-=1;break;case'bottom':this.body[0].y+=1;break;}}
• 在game中测试
this.snake.move(this.food,this.map);this.snake.render(this.map);
• 私有方法
什么是私有方法?不能被外部访问的方法如何创建私有方法?使用自调用函数包裹
• 在game.js中添加runSnake的私有方法,开启定时器调用蛇的move和render方法,让蛇动起来
• 判断蛇是否撞墙
functionrunSnake() {var timerId =setInterval(function() {this.snake.move(this.food,this.map);// 在渲染前,删除之前的蛇this.snake.render(this.map);// 判断蛇是否撞墙var maxX =this.map.offsetWidth / this.snake.width;var maxY =this.map.offsetHeight / this.snake.height;var headX =this.snake.body[0].x;var headY =this.snake.body[0].y;if (headX <0|| headX >= maxX) {clearInterval(timerId);alert('Game Over');}if (headY <0|| headY >= maxY) {clearInterval(timerId);alert('Game Over');}}.bind(that),150);}
• 在snake中添加删除蛇的私有方法,在render中调用
functionremove() {// 删除渲染的蛇var i =elements.length-1;for(; i >=0; i--) {// 删除页面上渲染的蛇 elements[i].parentNode.removeChild(elements[i]);// 删除elements数组中的元素elements.splice(i,1);}}
• 在game中通过键盘控制蛇的移动方向
functionbindKey() {document.addEventListener('keydown',function(e) {switch (e.keyCode) {case37:// leftthis.snake.direction='left';break;case38:// topthis.snake.direction='top';break;case39:// rightthis.snake.direction='right';break;case40:// bottomthis.snake.direction='bottom';break;}}.bind(that),false);}
• 在start方法中调用
bindKey();
// 在Snake的move方法中// 在移动的过程中判断蛇是否吃到食物// 如果蛇头和食物的位置重合代表吃到食物// 食物的坐标是像素,蛇的坐标是几个宽度,进行转换var headX =this.body[0].x*this.width;var headY =this.body[0].y*this.height;if (headX ===food.x&& headY ===food.y) {// 吃到食物,往蛇节的最后加一节var last =this.body[this.body.length-1];this.body.push({x:last.x,y:last.y,color:last.color})// 把现在的食物对象删除,并重新随机渲染一个食物对象food.render(map);}
避免html中出现js代码
(function (window,undefined) {var document =window.document;}(window,undefined))
• 传入window对象
将来代码压缩的时候,可以吧 function (window) 压缩成 function (w)
• 传入undefined
在将来会看到别人写的代码中会把undefined作为函数的参数(当前案例没有使用) 因为在有的老版本的浏览器中 undefined可以被重新赋值,防止undefined 被重新赋值
现在的代码结构清晰,谁出问题就找到对应的js文件即可。通过自调用函数,已经防止了变量命名污染的问题
但是,由于js文件数较多,需要在页面上引用,会产生文件依赖的问题(先引入那个js,再引入哪个js) 将来通过工具把js文件合并并压缩。现在手工合并js文件演示
• 问题1
// 如果存在多个自调用函数要用分号分割,否则语法错误// 下面代码会报错(function () {}())(function () {}())// 所以代码规范中会建议在自调用函数之前加上分号// 下面代码没有问题;(function () {}());(function () {}())
• 问题2
// 当自调用函数前面有函数声明时,会把自调用函数作为参数// 所以建议自调用函数前,加上;var a =function () {alert('11');}(function () {alert('22');}())
• 现实生活中的继承
• 程序中的继承
functionPerson (name, age) {this.type='human'this.name= namethis.age= age}functionStudent (name, age) {// 借用构造函数继承属性成员Person.call(this, name, age)}var s1 =Student('张三',18)console.log(s1.type,s1.name,s1.age) // => human 张三 18
functionPerson (name, age) {this.type='human'this.name= namethis.age= age}Person.prototype.sayName=function () {console.log('hello '+this.name)}functionStudent (name, age) {Person.call(this, name, age)}// 原型对象拷贝继承原型对象成员for(var key inPerson.prototype) {Student.prototype[key] =Person.prototype[key]}var s1 =Student('张三',18)s1.sayName() // => hello 张三
functionPerson (name, age) {this.type='human'this.name= namethis.age= age}Person.prototype.sayName=function () {console.log('hello '+this.name)}functionStudent (name, age) {Person.call(this, name, age)}// 利用原型的特性实现继承Student.prototype=newPerson()var s1 =Student('张三',18)console.log(s1.type) // => humans1.sayName() // => hello 张三
本文来源:https://www.2haoxitong.net/k/doc/79b724254b7302768e9951e79b89680203d86b39.html
文档为doc格式