javaScript教程

一、发展历程

javaScript是Netscape为了解决互联网初期网速过慢,而导致用户体验差而诞生的用在客户端语言。
比如一次用户输入提交等待几十秒后,服务器悠哉悠哉的返回了个‘xxx是必填项’。
而javaScript则将这些逻辑优先处理在客户端。

在Netscape将JavaScript发展到1.1的时候,微软决定进军浏览器,发布了 IE 3.0 并搭载了一个 JavaScript 的克隆版,叫做 JScript。

在微软进入后,有 3 种不同的 JavaScript 版本同时存在:Netscape Navigator 3.0 中的 JavaScript、IE 中的 JScript 以及 CEnvi 中的 ScriptEase。

与 C 和其他编程语言不同的是,JavaScript 并没有一个标准来统一其语法或特性,而这 3 种不同的版本恰恰突出了这个问题。随着业界担心的增加,这个语言的标准化显然已经势在必行。

Netscapek也看到了这点,想把自己的JavaScript作为其规范和标准,于是就将其v1.1版本交给了欧洲计算机制造商协会,该协会指定 39 号技术委员会来完成此工作。

他们经过数月的努力完成了,将该规范名为 ECMAScript(ECMA-262),由此这种伟大的脚本语言规范标准诞生了。
第二年, 国标标准化组织和国际电工委员会也采用了 ECMAScript 作为标准,即 ISO/IEC-16262。
自此,浏览器开发商就开始致力于将 ECMAScript 作为各自 JavaScript 实现的基础,也在不同程度上取得了成功。
开始的时候,脚本语言优先于其规范,后来ECMAScript速度基本上就快于脚本的具体实现了。
往往规范出了很久,客户端浏览器实现才慢慢支持,这里不得不夸nodejs了,这个服务端脚本语言的实现总是能紧跟ECMAScript的步伐。
ECMAScript 6时,标准委员会决定由以前 ‘几年发布一个版本’ 改为 ‘每年6月份发布一个版本’,版本号改为ECMAScript +当前年份,立刻执行,由于当时正值2015年所以就改为了ECMAScript2015。所以由此可得出目前最新的是ECMAScript2018。
参考:
http://es6.ruanyifeng.com/#docs/intro,
https://www.jianshu.com/p/11b58d1bfeed

二、数据类型

都有哪些

5种简单数据类型:Undefined, Null, Boolean, Number, String.
1种复杂数据类型:Object.
上面的5种简单数据类型又称为基本数据类型,复杂数据类型又称为引用数据类型。基本数据类型保存在栈内存,引用数据类型实际上是一个指针,这个指针也保存在栈中,但是这个指针指向的对象则保存在堆内存中。

基本类型Number

Number类型为数值,用来表示整数和小数,并且支持各种进制格式

var a=0.1;//浮点型
var b=2;//整型
var c=070;//八进制的56

基本类型Boolean

布尔类型就两个值:true、false。具体也没啥可讲的。

基本类型String

字符串特点

ECMAScript 中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变
某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量

var lang = "Java";
lang = lang + "Script";
/**以上示例中的变量 lang 开始时包含字符串"Java"。而第二行代码把 lang 的值重新定义为"Java"
与"Script"的组合,即"JavaScript"。实现这个操作的过程如下:首先创建一个能容纳 10 个字符的
新字符串,然后在这个字符串中填充"Java"和"Script",最后一步是销毁原来的字符串"Java"和字
符串"Script",因为这两个字符串已经没用了。这个过程是在后台发生的**/

还有一点需要注意的是,这里我们摒弃了其他语言中,字符串是引用类型的特点

转换为字符串

转换字符串有两种办法。toString()、String()

toString()

toString()函数用于将当前对象以字符串的形式返回。该方法属于Object对象,由于所有的对象都"继承"了Object的对象实例,因此几乎所有的实例对象都可以使用该方法。
需要稍微留意的是,因为null 和 undefined并没有对应的包装类,所以它们就只是基本的数据类型,无法装箱成对象,自然也就没有此方法。

var result=undefined.toString();
console.log(result)
//Uncaught TypeError: Cannot read property 'toString' of undefined
var date=new Date();
var result=date.toString();
console.log(result)
//"Tue Apr 03 2018 15:40:25 GMT+0800 (CST)"

String()

String是一个全局方法,无需对象调用,即可使用。而且能够弥补前一个方法的不足

var result=String(undefined);
console.log(result)
//"undefined"
var date=new Date();
var result=String(date);
console.log(result)
//"Tue Apr 03 2018 15:40:25 GMT+0800 (CST)"

本章参考

https://www.cnblogs.com/fybsp58/p/5683206.html

基本类型null

null 值表示一个空指针对象
Null 类型是只有一个值null
typeof 操作符检测 null 返回"object"

什么时候用?

如果定义的变量准备在将来用于保存对象,那么最好将该变量初始化为 null 而不是其他值。这样一来,只要直接检查 null 值就可以知道相应的变量是否已经保存了一个对象的引用。
不过奇怪的是,ecma好像并没有空指针异常这么一说,如下代码并不会抛出空指针异常

//人类
function Person(name) {
    this.name=name;
}
//运行
var p=null;
console.log(p.name)
//Uncaught TypeError: Cannot read property 'name' of null

但是java却会

//Person类
class Person {
    String name;
    public Person(String name){
        this.name=name;
    }
}
//运行
public class Run{
    public static void main(String[] args) {
        Person p=null;
        System.out.println(p.name);
    }
}
//Exception in thread "main" java.lang.NullPointerException at file.main(file.java:9)

基本类型undefined

该类型只有一个值:undefined。
如果一个变量只声明,不赋值。默认此值将会为undefined

var i;
console.log(i)
//undefined

与Null的关系

undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true。
尽管 null 和 undefined 有这样的关系,但它们的用途完全不同
null一般指空的对象,而undefined一般用于声明为初始化赋值的变量。

引用类型简介

传统面向对象语言中对应--->Ecma中的引用类型,或者对象
传统面向对象语言中类的对象对应--->Ecma中的引用类型的对象实例
引用数据类型都是Object对象,因为所有的引用类型都继承至Objec这个超级引用类型,江湖人称“始祖、超类”
Ecma可以允许你自定义引用类型(也叫对象,其实就是类)。当然为了方便,Ecma已经给你内置了几个常用的引用类型:Array,Date,Function,3种基本数据类型的包装类型。其他传统语言也一样,这点并不稀奇

引用类型Object

Object 对象自身用处不大,不过在了解其他类之前,还是应该了解它。因为 Ecma 中的 Object 对象与 Java 中java.lang.Object 相似,Ecma 中的所有对象都由这个对象继承而来,Object 对象中的所有属性和方法都会出现在其他对象中,所以理解了 Object 对象,就可以更好地理解其他对象。
就像 Java 中的 java.lang.Object 一样,Object对象是其他所有对象的祖先,其他对象均继承此对象的方法和属性。那么Objec对象(也叫引用类型,类)包含如下方法和属性:

  • constructor:对创建对象的函数的引用(指针)。对于 Object 对象,该指针指向原始的 Object() 函数。
  • prototype:对该对象的对象原型的引用。对于所有的对象,它默认返回 Object 对象的一个实例。
  • hasOwnProperty(property):判断对象是否有某个特定的属性。必须用字符串指定该属性。(例如o.hasOwnProperty("name"))
  • IsPrototypeOf(object):判断该对象是否为另一个对象的原型。
  • PropertyIsEnumerable:判断给定的属性是否可以用 for...in 语句进行枚举。
  • ToString():返回对象的原始字符串表示。对于 Object 对象,ECMA-262 没有定义这个值,所以不同 Ecma 实现具有不同的值。
  • ValueOf():返回最适合该对象的原始值。对于许多对象,该方法返回的值都与 ToString() 的返回值相同。

需要留意点的是:从技术角度讲, ECMA-262 中对象的行为不一定适用于 JavaScript 中的其他对象。浏览器环境中的对象,比BOM 和 DOM 中的对象,都属于宿主对象,因为它们是由宿主实现提供和定义的。 ECMA-262 不负责定义宿主对象,因此宿主对象可能会也可能不会继承 Object。

引用类型Array

暂无介绍

引用类型Date

暂无介绍

引用类型3个包装类

暂无介绍

引用类型自定义

创建自定义的对象(类)和对象实例(对象)

function Person(name,age) {
    this.name=name;
    this.age=age;
}
var p=new Person('丁少华',20);
console.log(p)

两种数据访问方式

基本数据类型:按值访问,读写的是它们实际保存的值。
引用数据类型:按引用访问,读写它们时需要先从栈中读取堆内存地址,然后找到保存在堆内存中的值。

两种类型的复制

基本数据类型变量的复制:从一个变量向另一个变量复制时,会在栈中创建一个新值,然后把值复制到为新变量分配的空间中。
引用数据类型变量的复制:复制的是存储在栈中的指针,将指针复制到栈中为新变量分配的空间中,而这个指针副本和原指针指向的是同一个堆内存中的对象;复制操作后两个变量实际上将引用同一个对象,因此改变其中一个将影响到另外一个。

基本包装类型

学过java的就更好理解了,拆箱、装箱吧啦吧啦...
es沿袭了java语言中基本数据类型不是面向对象的思想,这在实际使用时存在很多的不便。为了解决这个不足,在设计类时为某些基本数据类型设计了一个对应的类进行代表(Number、String和Boolean),这样和基本数据类型对应的类统称为包装类,然后就可以通过装箱的对象中的属性和行为操作基本数据了。---它们是特殊的引用类型,既与其他的引用类型相似,同时又具有与各自的原始类型相应的特殊行为。比如:

var a="hello";
var res=a.substring(0,2);
console.log(res);//"he"
//上面这个例子中,变量a是一个字符串,字符创必然是基本类型的值,但是它却在第二行调用了substring()方法,并将结果返回保存在res中,
// 我们都知道,既然a是基本类型的值而不是对象,因而从逻辑上讲,它是没有方法的。
// 其实在在实现这个操作的时候,内部已经隐式地帮我们帮创建了一个包装对象了,java把这个过程叫做'自动装箱'
// 所以以上的实际的情形应该是这样的:
var a=new String("hello");
var res=a.substring(0,2);
console.log(res);//"he"

在实际运用中,我们可以显式的调用String、Number和Boolean来创建基本包装类型的对象,不过,应该在绝对必要的时候再这么做,因为这种做法很容易让人分不清自己是在处理基本类型还是引用类型。对基本包装类型的实例调用 typeof 会返回object
参考:
https://www.cnblogs.com/iamswf/p/4736760.html
http://blog.csdn.net/qq_31655965/article/details/51597285
http://blog.csdn.net/molly_xu/article/details/51194656
https://www.cnblogs.com/john-sr/p/5731247.html
https://www.cnblogs.com/future-zmy/p/6105362.html

三、语句

ecma也像其他所有语言一样,制定了语句,也叫流程控制语句
而且这些语句的语法和关键字和php、java他们都是通用的

都有哪些?

  • if条件判断语句
  • for循环语句
  • while循环语句
  • switch开关语句
  • try/catch捕获异常语句

需要留意作用域?

作用域永远都是任何一门编程语言中的重中之重,因为它控制着变量与参数的可见性与生命周期。
在ES6之前,Ecma没有块级作用域,只有全局作用域和函数作用域。
也就是说,在代码块(代码块是由两个花括号组成的,一对花括号{}即为一个块级作用域)内是没有作用域的。在代码块内声明的变量,将会在外部也能访问。当然之后es6的let修复了这一个问题

if(true){
    var i=6;
}
console.log(i)
//6

本章参考

https://segmentfault.com/a/1190000007650548

四、函数

函数创建

方式1:函数声明

//函数定义
function Say() {
    console.log('你好')
}
Say()//函数调用
//你好

方式2:函数表达式

//函数定义
var Say=function () {
    console.log('你好')
}
Say()//函数调用
//你好

这两者的区别请看6.其它>提升

关于签名

Ecma规定函数的参数是由包含0~多个值的数组来表示的。它所谓的形参只是提供便利,但不是必须的。
在其他语言中,命名参数这块必须要求事先创建函数签名,而将来的调用也必须与该签名一致。比如:Java中,方法签名包括函数名和参数列表,不包括返回值,编译器通过方法签名判断方法的调用,如果方法签名包括返回值,那么同名同参数不同返回值的方法被调用时编译器无法做出正确选择。
Ecma没有这些条条框框,解析器不会验证命名参数,所以说Ecma得函数没有签名。
正因如此,我们的函数无法被重载或重写
Ecma

//函数定义
function Say() {
    console.log('你好')
}
Say('丁少华')//只根据方法名
//你好

java

public class Test {
    static void say(){
        System.out.println("你好");
    }
    //ecma里,不能重载,因为没签名的原因等于重新定义了一遍
    static void say(String name){
        System.out.println("你好,"+name);
    }
    //主方法
    public static void main(String []args) {
       say("丁少华");//根据方法名、参数,所以选择执行了第二个重载的say函数。
    }
}
//你好,丁少华

函数也是对象

函数实际上是Function类型的实例,因此函数也是对象;而这一点正是JavaScript最有特色的地方。由于函数是对象,所以函数也拥有方法,可以用来增强其行为。

函数参数的传递

与其他程序设计语言不同,ECMAScript 不会验证传递给函数的参数个数是否等于函数定义的参数个数。开发者定义的函数都可以接受任意个数的参数(根据 Netscape 的文档,最多可接受 255 个),而不会引发任何错误。任何遗漏的参数都会以 undefined 传递给函数。形参显得无关重要,真正的参数是以一个函数内隐式的特殊对象 arguments来获取的,arguments.length就是实际参数的个数。
关于参数传递,js高程就一句话:ECMAScript 中所有函数的参数都是按值传递的。---这个跟java是一样的。
基本类型值的传递如同基本类型变量的复制一样,会在栈区copy一份副本并开辟存入
引用类型传递也是值传递,只不过是引用的值,注意并不是引用本身
基本数据类型
典型的值传递,其实就是copy一份在栈中

function myFun(name) {
    name=234;
    console.log('myFun:'+name)
}
var name=123;
myFun(name)
console.log('widow:'+name)
//myFun:234
//widow:123

引用数据类型
引用类型传递也是值传递,只不过是copy了引用指针的值(copy了一份,在占内存开辟新空间,跟基本类型一样),注意并不是引用本身

function myFun(wife) {
    wife.name='小青';
    console.log('myFun'+wife.name)
}
var wife={name:'小红',age:20};
myFun(wife)
console.log('widow'+wife.name)
// myFun小青
// widow小青

如何证明不是传入的引用本身呢

function myFun(wife) {
    //此时myFun变量wife引用的指针指向一个新指向堆中的实例,已经跟外部的那个wife堆断开关联了
    wife={name:'小青',age:20};
    console.log('myFun'+wife.name)
}
var wife={name:'小红',age:20};
myFun(wife)
console.log('widow'+wife.name)
// myFun小青
// widow小红

闭包

666

五、面向对象

简介

Ecma中没有类的概念,对象都是基于引用类型创建的,这个引用类型(创建新对象的对象)可以是原生引用类型,也可以是开发人员定义的类型。虽然没有类的概念,但是可以把引用类型用于看作类。
java中自定义的类

class Person {
    String name;
    Person(String name){
        this.name=name;
    }
    public static void main(String[] args) {
        Person p=new Person("丁少华");
        System.out.println(p.name);
    }
}
//丁少华

ecma中自定义的引用类型,也叫做对象(即创建对象的对象,哈哈是不是很绕,就是要绕晕你,这也是为啥es6引入class的原因了)

function Person(name) {
    this.name=name;
}
var p=new Person('丁少华');//创建Person对象的实例p
console.log(p.name)
//丁少华

Ecma中原生引用类型有:Object、Array、Date、Function、RegExp、3个包装类型。Object是一个基础类型,其他所有类型都从Object类型继承了基本的行为。
面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。前面提到过,Ecma中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。它不具备传统的面向对象语言所支持的类和接口等基本结构。
在Ecma中,引用类型是一种数据结构,用于将数据和功能组织在一起,描述的是一类对象所具有的属性和方法,这点看起来和传统语言的类很相似,但称其为类并不妥当,因为这是传统面向对象语言的叫法,为严谨我们可以称为对象模板定义、引用类型定义。
如上Person是一个函数,因为是用于定义对象模板、引用类型的,所以我们也叫它构造函数。任何函数,只要通过new操作符来调用,那它就可以作为构造函数。new操作符调用构造函数创建对象的步骤?

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 隐式返回新对象

对象

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。对象是引用类型的一个实例,每个对象都是基于一个引用类型创建的。
Ecma中,一切皆对象。Person也是对象,p也是对象。Person是Function的对象,p是Person对象
为了方便理解,在特定的环境下,我们把Person叫做对象,把p叫做Person对象实例(避免陷入‘对象的对象’叫法的绕口尴尬)
内置的对象:Array、Date...也都对象,是Function类型的对象,这点跟Person一样原理。
最简单的创建对象就是创建一个Object对象的实例,比如var obj=new Object(),或者简写为var obj={ },参考js高级编程6.1

注意叫法

//创建了Person对象或者Person引用类型
function Person(name) {
    this.name=name;
}
var p=new Person('丁少华');//创建Person对象的实例p
console.log(p.name)
//丁少华

觉得很拗口,但是也得记
Ecma把创建的类叫做对象,比如我创建了个Person类,它却叫做创建了个Person对象或者Person引用类型。
而p却只能叫做Person对象实例
关于Person,Ecma会出现很多叫法:对象定义、对象模板定义、引用类型定义、对象的配方、对象。其实本质上跟传统语言的类差不多
这一点,我就是从传统面向对象语言上转过来的,困饶了我将近3年的概念。大部分资料称呼太乱了,而且本身的思想转变也很困难。

Function与Object

(此段过于深入,先搁浅)
关于Function与Object之间的关系,我觉得就是鸡生蛋,蛋生鸡的问题。
Object是(构造)函数,因为函数又是Function的对象,所以它又是对象,结构如下:

function Object(){native code}

Function肯定是个对象,因为

Ecma中,一切皆对象,Function肯定是另一个函数的对象

所以下边成立

console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true

constructor

每一个实例对象都拥有此属性,原形对象上也有此属性。

var Person=function () {
    this.age=17;
    this.getTime=function () {
        return new Date();
    }
}
var p=new Person();
console.log(p.constructor)
console.log(Person.prototype.constructor)
//打印两次
// var Person=function () {
//     this.age=17;
//     this.getTime=function () {
//         return new Date();
//     }
// }

不过需要注意的是,它只是一个属性,属性值是可以随时改变的,所以说,constructor具有不确定性,是可以后期修改或覆盖的,但即使修改了,也不会影响当前Person创建对象的功能

var Person=function () {
    this.age=17;
    this.getTime=function () {
        return new Date();
    }
}
//重写Person上的constructor属性
Person.prototype.constructor=function () {
    this.sex='girl';
}
//但是一点并不影响Person作为构建对象实例的功能,只是Person上的属性constructor变了而已
var p=new Person();
console.log(p.age,p.sex)
console.log(p.constructor)
//打印如下
//17 undefined
// var Person=function () {
//     this.age=17;
//     this.getTime=function () {
//         return new Date();
//     }
// }

类方法与实例方法

我知道ecma不这么叫,但是我实在找不出有什么合适的叫法,毕竟es没有定义这些概念,但是却有这些相似的语法。

类方法

就是用类直接调用,不过与java不同的是,实例却无法调用,会报错

function Calculator() {}
//类方法
Calculator.add=function (x,y) {
    return x+y;
}
var result=Calculator.add(2,3);
console.log(result)
//5

实例方法

需要用该类的实例去调用,以下两种写法都行

function Calculator(x,y) {
    this.x=x;
    this.y=y;
}
//原型方法
Calculator.prototype.add=function () {
    return this.x+this.y;
}
var cal=new Calculator(2,3);
var result=cal.add();
console.log(result)
//5
function Calculator(x,y) {
    this.x=x;
    this.y=y;
    //对象方法(也叫自有属性) 
    this.add=function () {
        return this.x+this.y;
    }
}
var cal=new Calculator(2,3);
var result=cal.add();
console.log(result)
//5

实例单个属性/方法

还有一种我个人称之为单个实例单个成员
单个实例定义的属性,只能自己存储用,其他同类成员都不能用,也不影响类函数,我觉得它存在的意义不是太大
优点是随意,缺点是无法公用形成模板

//Bird类
function Bird(name) {
    this.name=name;
}
//测试
var b=new Bird('小兰');
b.age=20;//这货这存在于此对象中,跟外部任何东西都没关联
console.log(b)
console.log(b.name)
console.log(b.age)

由此也可以推论出类属性与实例属性这里不再多说。jq的$.extend是拓展静态方法,而$.fn.extend是拓展实例方法也是基于此原理的
参考:
https://blog.csdn.net/songmaolin_csdn/article/details/52861361
https://www.cnblogs.com/tracyzeng/p/5553715.html

创建对象

工厂模式

考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节

function Person(name) {
    var obj=new Object();
    obj.name=name;
    obj.say=function () {
        console.log('哈哈,我叫'+this.name)
    };
    return obj;
}
var p=new Person('丁少华');
p.say()
//哈哈,我叫丁少华

缺点:无法知道对象的类型,因为工厂内部返回的都是Object类型

构造函数模式

前边说过过,Ecma中的构造函数可用来定义对象模板从而创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person(name) {
    this.name=name;
    this.say=function () {
        console.log('哈哈,我叫'+this.name)
    };
}
var p=new Person('丁少华');
p.say()
//哈哈,我叫丁少华
console.log(p instanceof Person)
//true

如上,通过instanceof 操作符可检测当前对象的类型。创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。与其还有差别的是没有显式地创建对象、直接将属性和方法赋给了this对象、没有return语句。

function Person(name) {
    this.name=name;
    this.say=function () {
        console.log('哈哈,我叫'+this.name)
    };
}
var p1=new Person('丁少华');
var p2=new Person('王新');
console.log( p1.say==p2.say)
//false

缺点:同一个类型(对象)的实例之间,无法共享方法和属性。属性自然没什么,但是作为同样功能的方法,就不该被创建两份存在内存,这样会造成资源浪费。

function Person(name) {
    this.name=name;
    this.say=function () {
        console.log('哈哈,我叫'+this.name)
    };
    /**
    *上边的方法这样写,更加能体现缺点。p1和p2都有一个名为say()的方法,但那两个方法不是同一个Function的实例
    *this.say=new Function("console.log('哈哈,我叫'+this.name)");
    **/
}
var p1=new Person('丁少华');
var p2=new Person('王新');
console.log( p1.say==p2.say)
//false

原型模式

理解原型

只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。
该原型对象会自动获得一个constructor属性,这个属性又反指回了此函数。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指向该构造函数原型对象的指针,如上图
对象实例共享其原型对象上的属性和方法
prototype是来拯救我们的,只需把要共享的变量、函数放到它下边。
更深层次的理解原型,请看继承章节

原型模式

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person() {}
    Person.prototype.name='jack';
    Person.prototype.say=function () {
    console.log('哈哈,我叫'+this.name)
};
var p1=new Person();
var p2=new Person();
console.log(p1.name,p2.name,p1.say==p2.say)
//jack jack true

缺点:
对于那些包含基本值的属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。

组合模式

基于构造和组合模式的优缺点,一种完美的idea诞生了。组合模式:即不共享属性,同时却共享了方法。是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法

function Person(name) {
    this.name=name;
}
Person.prototype.say=function () {
    console.log('哈哈,我叫'+this.name)
};
var p1=new Person('丁少华');
var p2=new Person('王新');
console.log(p1.name,p2.name,p1.say==p2.say)
//丁少华 王新 true

继承

一个子类对象可以获得其父类的所有属性和方法,称之为继承
继承是面向对象语言中的一个最为人津津乐道的概念,许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在Ecma中无法实现接口继承。Ecma只支持实现继承,而且其实现继承主要是依靠原型链来实现的。---摘自JavaScript高级程序设计第三版
老实说,我并不晓得接口继承和实现继承这个概念,以为这是出自于c++的,本人熟悉java,但是java没有此概念,
但是Ecma’函数没有签名‘这个我是知道的,所以不深究的话,这个地方能走通。如果深究,还要去深度学习c++,成本太高,所以暂时就不深究
java通过关键字extend来实现class之间的继承的。但是Ecma并没有class的概念,也无extends关键字,所以Ecma中描述了原型链的概念,并将原型链作为实现继承的主要方法。所以下边主要就是讲这个。当然es6之后有了class、extends等概念,但是究其根源,只是语法糖而已

原型链继承

原型

如下代码:

function Person(name) {
    this.name=name;
    this.say=function () {
        console.log('哈哈,我叫'+this.name)
    };
}
var p1=new Person("丁少华");
var p2=new Person("王新");
//本质上等价于
function Person(name) {
    this.name=name;
    this.say=new Function("console.log('哈哈,我叫'+this.name)");
}

这样的话,内存结构将是下边这样
不同于其他语言,Ecma规定方法也是对象。比如java中,方法是一种特殊的存在,会放入内存中的方法区内,有且只有一份。如上图,say函数本应该是Person类型实例所共享的,但是因为Ecma中方法也是对象的原因,会在内存中开辟两份空间,这就造成了资源浪费。为了解决这个问题,Ecma发明了一种新的概念---原型。
所以,以上代码最终的内存是这个样子的。为了不让看官觉得太乱,下边的图我就不再描述栈和堆。下边是各个对象之间的关系
prototype是来拯救我们的,只需把要共享的变量、函数放到它下边,就可以节省内存空间,那么以下是关于原型的总结:

  • 只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。
  • 该原型对象会自动获得一个constructor属性,这个属性又反指回了此函数。
  • 当调用构造函数创建一个新实例后,该实例的内部将包含一个指向该构造函数原型对象的指针
  • 对象实例共享其原型对象上的属性和方法(先找自己实例中的自由属性,如果没有就会向原型对象上查找,如果还没有就返回undefined,比如下段代码:)
function Person() {
    //自有属性
    this.name='小明';
}
//对象会继承原型里的属性
Person.prototype.age=20;
var p=new Person();
//打印结果可看到其自有属性和继承自原型的属性
console.log(p.name,p.age,p.sex)//小明,20,undefined

参考:https://segmentfault.com/a/1190000003057229

通过原型链继承

Ecma中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本念。

//Animal类
function Animal() {
    this.eat=function () {
        console.log(this.name+',吃')
    };
}
Animal.prototype.sleep=function () {
    console.log(this.name+',睡觉')
}
//Bird类
function Bird(name) {
    this.name=name;
}
//有意思的是,原型继承也只是单继承,如果默认情况下是直接继承Object的,如果我这样,则会在中间增加一环继承,然后
//继承的那个父类载继承Object,如果还有一环,那就依次类推。
//当前的类会继承父亲,爷爷,爷爷的爷爷...
//所以最开始的那个肯定继承Object,这也是为啥说,所有的对象均继承自Oject的原因了
Bird.prototype=new Animal();
Bird.prototype.say=function () {
    console.log(this.name+',呵呵')
}
//测试
var b=new Bird('小兰');
b.eat()
// b={
//     name:s,
//     当new 一个Animal的时候,Bird就原型就指向堆内存中的一个Animal对象了
//     __proto__:=
//        --- new Animal:{--- 此处返回的是Animal类型的对象实例,所以chrome会输出__proto__:Animal
//                 eat:f,
//                 say:f,当执行Bird.prototype.say的时候,实际上是向那个Animal对象上添加一个函数
//                 __proto__:{
//                      sleep:f,
//                  }
//
//       ---  }---
//     }
// }

是的,不要不理解,原型对象可以重写,下边这样演进,或许能帮你更容易的理解。

//Bird类
function Bird(name) {
    this.name=name;
}
Bird.prototype={
    sex:'girl',
    say:function () {
        console.log('呵呵')
    }
};
//Bird类
function Bird(name) {
    this.name=name;
}
var obj=new Object();
obj.sex='girl';//这个样子java中是不允许的,需你提前要在类中定义此属性,然后通过点设置或读取,但是ecma可以
obj.say=function () {
    console.log('呵呵')
}
Bird.prototype=obj;
//动物类
function Animal() {}
//Bird类
function Bird(name) {
    this.name=name;
}
var obj=new Animal();//继承个自定义的类,不觉得Object太单一了嘛
obj.sleep=function () {
    console.log('睡觉')
}
Bird.prototype=obj;
//动物类
function Animal() {
    //把共用的挪到类里边,当然,这个到Animal原型上更好
    this.sleep=function () {
        console.log('睡觉')
    }
}
//Bird类
function Bird(name) {
    this.name=name;
}
var obj=new Animal();
Bird.prototype=obj;
//动物类
function Animal() {}
Animal.prototype.sleep=function () {
    console.log('睡觉')
}
//Bird类
function Bird(name) {
    this.name=name;
}
var obj=new Animal();
Bird.prototype=obj;

如下图:1、2、3、4表示查找优先级。1最高

//Animal类
function Animal() {
    this.fun=function () {
        console.log(3)
    };
    this.eat=function () {
        console.log('吃')
    };
}
Animal.prototype.sleep=function () {
    console.log('睡觉')
}
Animal.prototype.fun=function () {
    console.log(4)
}
//Bird类
function Bird() {
    this.fun=function () {
        console.log(1)
    }
    this.sing=function () {
        console.log('歌')
    }
}
Bird.prototype = new Animal();//通过这样,Bird将继承Animal对象(包含普通成员如languge和原型对象如sleep)
Bird.prototype.fly=function () {
    console.log('飞')
};
Bird.prototype.fun=function () {
    console.log(2)
};
//测试
var b=new Bird('迪巴');
b.fun()
b.sing()
b.fly()
b.eat()
b.sleep()
console.log(b)
//原型链结构如下,
// b= {
//     fun:f1,
//     sing:f,
//     __prop__:{
//          fun:f2,
//          fly:f
//         --new Animal:{---//此处为虚线,表示逻辑存在关系,但是并不会显示出来。所以f2会覆盖f3
//             fun:f3,
//             eat:f,
//             __prop__:{
//                 fun:f4,
//                 sleep:f
//              }
//         ---}---
//     }
//
// }

通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制。读者大概还记得,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用b.fun()会经历三个搜索步骤: 1)搜索实例; 2)搜索 Bird.prototype;3)搜Animal.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
原型链继承的缺点
1、引用类型值的原型属性会被所有实例共享
对于那些包含基本值的属性倒
也说得过去,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属
性。然而,对于包含引用类型值的属性来说,问题就比较突出了。

//动物类
function Animal() {
    this.loves=['tv','game'];
}
//Bird类
function Bird() {}
Bird.prototype=new Animal();
//测试
var b1=new Bird();
b1.loves.push('girl');
console.log(b1.loves)
var b2=new Bird();
console.log(b2.loves)
// 在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了比如:
// 这个例子中的 Animal 构造函数定义了一个 loves 属性,该属性包含一个数组(引用类型值)。
// Animal 的每个实例都会有各自包含自己数组的 loves 属性。当 Bird 通过原型链继承了
// Animal 之后, Bird.prototype 就变成了 Animal 的一个实例,因此它也拥有了一个它自
// 己的 loves 属性-——-就跟专门创建了一个 Bird.prototype.loves 属性一样。但结果是什么呢?
// 结果是 Bird 的所有实例都会共享这一个 loves 属性。 而我们对 b1.loves 的修改
// 能够通过 b2.loves 反映出来,就已经充分证实了此缺点。
function Bird() {}
Bird.prototype.name='aaa';
Bird.prototype.teacher={
    name:'鸟老师'
};
var b1=new Bird();
b1.name='bbb';//这样是会直接会在当前对象实例中增加一个新属性,'也就是我曾经说的实例单个属性',并不会修改原型
b1.teacher.name='熊老师';//查找到了原型上的teacher属性,然后直接修改了其name值
var b2=new Bird();
console.log(b1)
console.log(b2)
console.log(Bird.prototype)

2、在创建子类型的实例时,不能向超类型的构造函数中传递参数。是的,只能继承无参构造
参考:
https://blog.csdn.net/ladycode/article/details/51282407
https://www.cnblogs.com/sarahwang/p/6870072.html

借用构造

原型继承两个问题,其一是如果父类成员(自有成员和原型对象上的成员)中有引用类型的类型,只要一个子类对象修改此属性将会影响到所有的对象中。其二,子类实例对象无法传参至父类。
综上所诉,于是借用构造函数(类)方法出现

apply()

因为 call()与此方法作用相同,故不再讲
这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和 call()方法也可以在将来新创建的对象上执行构造函数。就只是简单的借用一下函数而已,constructer也不变,原型什么的都不会变,什么么都不变。不要想太复杂了

//Animal类
function Animal(name,age) {
    this.name=name;
    this.age=age;
    this.eat=function () {
        console.log(this.name+',吃')
    };
}
//Bird类
function Bird(name,age,sex) {
    Animal.call(this,name,age)
    this.sex=sex;
}
//测试
var b=new Bird('小红',20,'女');
b.eat()
console.log(b)

如上:Bird类通过使用 call()方法,将要新创建的 Bird实例的环境下调用了 Animal 构造函数

缺点

ecma中的东西,总是有缺点
如果仅仅是借用构造函数,那么也将无法避免构造函数(类)模式存在的问题——方法都在构造函数(类)中定义,因此类中的实例方法复用就无从谈起了。而且,在父类的原型中定义的方法(上边说过,只是借用了类的当前环境,其他所有都不变),对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式(即浪费内存模式)。考虑到这些问题,借用构造函数(类)的技术也是很少单独使用的。

组合继承

组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方。它本身不是新技术法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
一般建议,实例属性会使用借用构造继承模式,实例方法是借用原型链继承模式。

//Animal类
function Animal(name,age) {
    this.name=name;
    this.age=age;
    this.eat=function () {
        console.log(this.name+',吃')
    };
}
Animal.prototype.sleep=function () {
    console.log(this.name+',😴')
}
//Bird类
function Bird(name,age,sex) {
    Animal.call(this,name,age)//借用构造继承
    this.sex=sex;
}
Bird.prototype=new Animal();//原型链继承
var b=new Bird('小红',20,'女');
b.eat()
b.sleep()
console.log(b)

理解如下
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 Ecmacript 中最常用的继 承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

两个属性

属性描述符是ES5中新增的概念,其作用是给对象的属性增加更多的控制。Es5定义的对象中有两种特殊的属性, 这两种特殊的属性在你定义对象属性时就会赋予, 我们在必要时可以改写这两种特殊的属性让其属性的访问更加的合理化。
要修改属性默认的特性,须使用Object.defineProperty(obj, prop, descriptor)进行配置。
要查看当前属性的特性,须使用Object.getOwnPropertyDescriptor(obj, prop)进行查询
不要看这货写法奇怪,因为它是类方法

数据属性

通过对数据属性的设置, 我们可以让用户操作普通的数据时限制他们可以操作的权限, 可以让其数据变得不可修改、不可在for...in中被其枚举、不可以删除该数据等等4个描述其行为的特性

修改特性--Writable

类型:布尔值
默认值:false
如果一个对象的某个属性,你一旦定义,就不想让别人再修改,那么可以用此特性(可以模仿常量)

var dsh={
    work:'教学',
    name:'丁老师'
}
dsh.work='学生';
//获取对象上一个自有属性对应的属性描述
var propDes=Object.getOwnPropertyDescriptor(dsh, 'work')
console.log(propDes)
console.log(dsh)
// value: "教学", writable: true, enumerable: true, configurable: true
//{
//   name:'丁老师',
//    work:'学生'
//}

如上,外部很容易就可以修改属性的值,但是有些属性我怎么让他不能修改呢?

var dsh={
    name:'丁老师'
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'work',{
    writable: false
})
//测试
dsh.work='学生';
console.log(dsh)
//{
//   name:'丁老师',
//   work:undefined
//}

默认值特性--value

类型:all
默认值:undefined

var dsh={
    name:'丁老师'
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'work',{
    value: '老师'
})
//测试
console.log(dsh)
console.log(dsh.work)
//{
//   name:'丁老师',
//    work:'老师'(work属性的颜色被chrome展示为灰色)
//}
//老师

枚举特性---enumerable

类型:布尔值
默认值:false
能否通过for-in循环返回属性

var dsh={
    name:'丁老师',
    work:'老师',
    age:20
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'age',{
    enumerable: false
})
//测试
console.log(dsh)
for(i in dsh){
    console.log(dsh[i])
}
//{
//   name:'丁老师',
//   work:'老师',
//   age:20
//}
//丁老师
//老师

配置属性---Configurable

类型:布尔值
默认值:false
该属性是否能被删除,并且无法再次配置属性的特性

var dsh={
    name:'丁老师',
    work:'老师',
    age:20
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'age',{
    configurable: false
})
//测试
delete dsh.age;//删除毫无作用
console.log(dsh)
//{
//   name:'丁老师',
//   work:'老师',
//   age:20
//}

需要注意的是:该特性除了让其无法删除属性之外,还有一个作用---一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误

访问器属性

数据属性一般用于存储数据数值,而我们的主角--访问器属性对应的是set/get操作,不能直接存储数据值。在读写属性时必定会进入此连个属性,如需特殊要求,就是通过这两个方法来进行操作处理的。除了enumerable、configurable这两个跟数据属性功能一样的之外,剩下的就是get和set特性了。

set

var dsh={
    name:'丁少华',
    work:'老师',
    _des:'暂无'//前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'des',{
    get: function(){
        var temp=this._des;
        if(this.work){//只有在有work的情况下哎.,我才重写描述des,否则还是默认
            temp=this.name+',职业是'+this.work;
        }
        return temp;
    }
})
//只指定getter意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了getter函数的属性会抛出错误.
//所以下面妄图改变属性值的做法并不有什么用
dsh.des='123'
//测试
console.log(dsh.des)
//丁少华,职业是老师

get

var dsh={
    name:'丁少华',
    work:'老师',
    _des:'暂无'
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'des',{
    set:function (v) {
        console.log(v)
        this._des=v;
    }
})
//只指定setter函数的属性也不能读,否则在非严格模式下会返回undefined,而在严格模式下会抛出错误
//因为上边只有set没有get,所以这个属性的值只能是undefined的了,所以下边设置也毫无意义
dsh.des='修改描述测试'
//测试
console.log(dsh.des)
//修改描述测试
//undefined
var dsh={
    name:'丁少华',
    work:'老师',
    _des:'暂无'
}
//设置对象上一个自有属性对应的属性描述
Object.defineProperty(dsh, 'des',{
    set:function (newv) {
        var oldv=this._des;
        var now=new Date();
        var temp=now+'修改了此属性的值,把['+oldv+']修改为['+newv+']';
        this._des=temp;//设置
    },
    get: function(){
        return this._des;
    }
})
dsh.des='描述测试'
//测试
console.log(dsh.des)
//修改描述测试
//Thu Apr 12 2018 01:50:13 GMT+0800 (中国标准时间)修改了此属性的值,把[暂无]修改为[描述测试]

由于为对象定义多个属性的可能性很大,Es 5又定义了一个Object.definePro- perties()方法。用法一致,没啥可讲的

几大对象

本地对象

也叫原生对象
定义为“独立于宿主环境的 ECMAScript 实现提供的对象”。简单来说,本地对象就是 ECMA-262 预定义的类也就是引用类型。

  • Object
  • Function
  • Array
  • String
  • Boolean
  • Number
  • Date
  • RegExp
  • Error
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

内置对象

内置对象也属于本地对象,区别在于:内置对象是不需要实例化的的对象,而其他内置对象本质上是类
Global
Global(全局)对象是ECMAScript中一个特别的对象,因为这个对象是不存在的。在ECMAScript中不属于任何其他对象的属性和方法,都属于它的属性和方法。所以,事实上,并不存在全局变量和全局函数;所有在全局作用域定义的变量和函数,都是Global对象的属性和方法。
Global无法访问,但是一般情况下,为了加强可操作性,Ecma解释器(也就是宿主环境)会提供显式的对象。比如在浏览器下是window、在nodejs中是Globa等等
Math
ECMAScript还为保存数学公式和信息提供了一个对象,即Math对象。与我们在JavaScript直接编写计算功能相比,Math 对象提供的计算功能执行起来要快得多

宿主对象

由ECMAScript实现的宿主环境提供的对象,可以理解为:浏览器提供的对象。所有的BOM和DOM都是宿主对象

posted @ 2023-01-12 11:01  丁少华  阅读(37)  评论(0编辑  收藏  举报