GitVP开源文摘
全部文章/前端开发

安邦的JavaScript学习笔记

JavaScript 学习笔记;JavaScript学习总结;前端变化太快,只有把JS基础打扎实了,才能游刃有余;为了让我的JavaScript基础异常牢固,思路更加清晰,我会把以前总结过的知识和平时看到的资料放在这里,把以前零零散散的知

作者anbang 仓库anbang/javascript-notes ↗ 星标★ 2,261 字数84,445 阅读1
GitHub 原文 ↗
摘要后期主要更新在我的个人网站上,感觉我整理后的还是很适合学习和阅读的,服务器放在国内,访问速度也超级快;

安邦的JavaScript学习笔记

这是当初学习JS时候的笔记,

因小伙伴反应Github不适合阅读,

我在我的个人网站(阿西河前端教程)上也有整理一份,详情参见;

朱安邦的JavaScript学习笔记 (我的个人网站)

后期主要更新在我的个人网站上,感觉我整理后的还是很适合学习和阅读的,服务器放在国内,访问速度也超级快;

然后每年的年底统一把新增的内容更新到当前的Github仓库;

欢迎大家关注我的小网站: https://www.axihe.com/

同时我也在Bilibili上开频道做UP主;欢迎在B站关注我: https://space.bilibili.com/59312814

基于ES5,包含ES6的,笔记最近正在重新整理,有些链接可能失效;会尽快整理好;
  1. 前端开发初识
- [编辑器的选择/开发环境介绍](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#one)
- [HTML文档和标签介绍](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#two)
- [JS由哪些部分组成、DOM初识](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#three)
- [HTML中怎么使用JS/CSS](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#four)
- [从noscript标签引深出的两种编程思想(优雅降低,渐进增强)](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#five)
- [javascript的文档模式和ES6](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#six)
- [变量的定义和储存值的类型判断](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#seven)
- [强类型语言和弱类型语言的区别](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#eight)
- [编程语言是做什么的?面向对象编程的真正意义是什么?](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#nine)
- [练习题](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#ten)
- [前端技术方向的选择,编程语言是做什么的?](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#eleven)
- [JS代码检测的一些小方法](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/前端开发初识/README.MD#twelve)
  1. 数据类型初识
- [String](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#one)
- [Number](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#two)
- [Boolean](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#three)
- [null和undefined](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#four) (数据类型详解的时候不再总结,已经写的差不多了)
- [Object](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#five)
- [null和undefined的区别和联系](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#six)
- [Symbol](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#footer)
- [练习题](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#footer)
  1. 函数初识
- [初识函数](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#one)
- [函数的定义](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#two)
- [函数的调用](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#three)
- [函数的方法](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#four)
- [arguments对象](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#five)
- [函数的参数](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#six)
- [this关键字的指向](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/数据类型初识/README.MD#seven)
  1. 变量声明和原理
- [变量的命名](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#one)
- [变量的声明](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#two)
- [变量的类型](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#three)
- [全局变量和局部变量](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#four)
- [注释和语法建议](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#five)
- [练习题](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/变量声明和原理/README.MD#footer)
  1. 表达式和运算符
- [算术运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#one)
- [自增与自减](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#two)
- [关系运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#three)
- [对象运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#four)
- [逻辑运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#five)
- [位运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#six)
- [其他运算符](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#seven)
- [=/==/===、!与!!的区别](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#eight)
- [计算器案例](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#nine)
- [表格排序案例](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#ten)
- [练习题](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/表达式和运算符/README.MD#footer)
  1. 流程语句
- [循环语句](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/语句/README.md#one)
- [跳转语句](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/语句/README.md#two)
- [选择语句](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/语句/README.md#three)
- [异常处理语句](https://raw.githubusercontent.com/anbang/javascript-notes/HEAD/语句/README.md#four)
  1. String类
  2. Array类
  3. Number类
  4. Boolean类
  5. Object类
  6. Function类
  7. Math类
  8. Data类
  9. Global对象
  10. RegExp类型
  11. 定时器

  • 错误处理和异常调试
  • 表单脚本
    • 基础知识
    • 文本和选择框
    • 表单序列化
    • 富文本编辑
  • AJAX
    • 服务器/浏览器编程的机制和原理,同步和异步交互
    • 基于nodeJs环境,用原生JS操作表单
    • jsonp跨域ajax,jQuery的AJAX原理和应用
  • 编写可维护的javascript代码,性能优化与部署

预解释的原理

预解释的原理

  • 预解释的不同机制
  • var的预解释机制
  • function的预解释机制
  • 预解释机制
  • in的用法
  • 面试题练习

预解释的的不同机制

预解释也叫预声明,是提前解释声明的意思;预解释是针对变量和函数来说的;但是变量和function的的预解释是两套不同的机制;

  • 当浏览器加载我们的HTML页面的时候,首先会提供一个供JS代码执行的环境->全局作用域global(浏览器中的全局作用域,也叫顶级作用域是window)
  • JS中的内存空间分为两种:栈内存、堆内存
- 栈内存;提供JS代码执行的环境,存储基本数据类型的值;->全局作用域或者私有的作用域其实都是栈内存;
- 堆内存;存储引用数据类型的值(对象是把属性名和属性值储存进去,函数是把函数体内的代码当做字符串储存进去)
  • 在当前的作用域中,JS代码执行之前,浏览器首先会默认的把所有代var和function的进行提前的声明或者定义,->"预解释"(也叫变量提升)

var的预解释机制

var a=1
  • 1、代码运行之前,先扫描有没有带var关键字的变量名,有的话,为这个变量名,在内存里开一个空间;这时候变量名a是不代表任何值的;用undefined来表示;undefined是一个标识符/记号,表示找不到这个变量名所代表的数据;不存在的意思;这个阶段叫变量的声明;
  • 2、当代码运行的时候,则给数据1开辟一个内存空间;
  • 3、让数据1和变量名a绑定在一起;变量类型指的就是数据类型;按照js语言的原理来说变量类型有undefined类型;但是数据类型是没有undefined这种数据类型的;只有"undecided"这种字符串类型(字符串类型是数据类型的一种);同理也没有unll这个数据类型,但是有"null"这种字符串类型;
    	 var num;
	    //1、声明(declare):var num; ->告诉浏览器在当前作用域中有一个num的变量了,如果一个变量只是声明了但是没有赋值,默认的值是undefined
	  console.log(num);//->undefined
	
	    num = 12;
	    //2、定义(defined):num=12; ->给我们的变量进行赋值
	  console.log(num);//->12
		//变量提前使用的话,就是undefined
	    console.log(testStr);//undefined
	    var testStr="22222222"

function关键字的预解释步骤

function fn(){……}

在代码执行之前,把所有的带function关键字的脚本都扫描一遍,然后定义变量;并且同时给变量赋值;

  • 1、函数的定义只是保存一些字符串;预解释的时候在内存里保存fn大括号里面的字符串;
  • 2、代码运行时候,读到fn()时候,这个时候就是函数的运行;函数的运行,会先开辟一个堆内存把字符串当做代码在堆内存中再次运行,函数产生的作用域内还会再进行预解释和代码运行;函数如果多次执行;会产生多个作用域;但是产生的多个作用域里面的内容都是相互独立的;互相没有关系;(在原型和原型链时候再仔细研究原理;)
		fn(100,200);//->可以在上面执行,因为预解释的时候声明+定义就已经完成了
		function fn(num1, num2) {
			var total = num1 + num2;
			console.log(total);
		}

总结

  • 1、var和function关键字的在预解释的时候操作还是不一样的
- var -> 在预解释的时候只是提前的声明了这个变量,只有当代码执行的时候才会完成赋值操作
- function -> 在预解释的时候会提前的把声明加定义都完成了(在代码执行的时候遇到定义的代码直接的跳过)
  • 2、预解释只发生在当前的作用域下,例如:开始只对window下的进行预解释,只有函数执行的时候才会对函数中的进行预解释;
[重要]刚开始只对window下的进行预解释,fn函数中目前存储的都是字符串,所以var total没啥实际的意义,所以不进行预解释 -> "预解释是发生在当前作用域下的"

  综合题;

		console.log(obj);//->undefined
		var obj = {name: "朱安邦", age: 25};
		function fn(num1, num2) {//代码执行到这一行的时候直接的跳过这一块的代码,因为在预解释的时候我们已经完成了声明加定义
			var total = num1 + num2;
			console.log(total);
		}
		var num1 = 12;
		fn(num1, 100);//执行fn,把全局变量num1的值赋值给形参num1,把100赋值给形参num2
  • 下面是一个预解释思路
		var a,
			b = 0,
			fn = function () {
				var a = b = 2;
			};
		fn();
		console.log(a, b);
把上面解析成下面就好理解了
		var a;
		window.b = 0;
		window.fn = function () {
			//var a = b = 2;
			var a = 2;//a是私有的和全局没关系
			b = 2;//b是全局的
		};
		fn();//window.fn()
		console.log(a, b);//undefined 2

预解释机制

  • 1、不管条件是否成立都要进行预解释
	    console.log(a);//->undefined
	    if (!!("a" in window)) {//"a" in window -> true
	    	var a = "朱安邦";
	    }
	    console.log(a);//->朱安邦
> 例子中的if是不成立的,预解释的时候,碰到非functon内的var,都会声明,无论你写在if else 还是别的判断里;结舌if
> 假设if语句起作用的话,那么第一次log a的时候,就会报错了(没有声明的变量,是不能直接用的,除非typeof),而声明并且没有赋值的表现才是undefined;假设不成立;
> 最开始总结的预解释步骤:**代码运行之前,先扫描有没有带var关键字的变量名,有的话,为这个变量名,在内存里开一个空间;**,预解释是发生在代码执行前的,所以if根本阻挡不了预解释;
  • 2、预解释只发生在"="的左边,只把左边的进行预解释,右边的是值是不进行预解释的
匿名函数之函数表达式:把函数定义的部分当做值赋值给一个变量或者元素的事件
	    fn1();//->undefined() Uncaught TypeError: fn is not a function JS中只有函数可以执行 && JS上面的代码如果报错了,在不进行任何的特殊处理情况下我们下面的代码都不在执行了
	  var fn1 = function () {
	      console.log("ok");
	  };
	  fn1();
	
	  //预解释的时候:fn=xxxfff000
	  fn2();//->"ok"
	  function fn2() {
	      console.log("ok");
	  }
	  fn2();//->"ok"

预解释的时候:var fn; ->fn的默认值是undefined;这里即使有functon,也是不能进行进行预解释的

  • 3、函数体中return下面的代码都不在执行了,但是下面的代码需要参加预解释;而return后面的东西是需要处理的,但是由于它是当做一个值返回的,所以不进行预解释;
	  var total = 300;
	  function fn() {
	    console.log(total);
	    return function sum() {};//return是把函数中的值返回到函数的外面,这里是把function对应的内存地址返回的到函数的外面,例如:return xxxfff111;函数体中return下面的代码都不在执行了
	    var total = 10;
	      console.log(total);
	  }
	  fn();// undefined
  • 4、匿名函数的function在全局作用域下是不进行预解释的;

匿名函数之自执行函数:定义和执行一起完成了;函数内的声明,只是在函数内使用;

		(function(num){
		  var testStr="test"+num;
		  console.log(num);
		})(100);
		console.log(testStr);// testStr is not defined
  • 5、在预解释的时候,如果遇到名字重复了,只声明一次,不重复的声明,但是赋值还是要重复的进行的

  预解释:

	var fn; 声明
	fn = xxxfff000; [声明]不要了+定义
	fn = xxxfff111; [声明]不要了+定义
	->fn=xxxfff111
	var fn = 12;//window.fn=12
	function fn() {//window.fn=function(){}
	}
	function fn() {
	}

  window下的预解释:

	fn=xxxfff111; 声明+定义
	var fn; 声明 (这一步不需要处理)
	fn=xxxfff222; 声明+定义(只需要执行定义即可)
	->fn=xxxfff222
一个案例;
fn();//->2
function fn() {
	console.log(1);
}
fn();//->2
var fn = 13;//var fn;跳过  fn=13(在预解释的时候没有执行这句话赋值的操作)
fn();//->13() Error:fn is not a function
function fn() {
	console.log(2);
}
fn();

JS中作用域只有两种:

  • window全局作用域、
  • 函数执行形成的私有作用域;
  • {name:""} if(){} for(){} while(){} switch(){} 这些都不会产生作用域;

ES6可以用let形成块级作用域;http://www.cnblogs.com/snandy/archive/2015/05/10/4485832.html

【in】:检测某一个属性名是否属于这个对象(不管是私有的属性还是公有的属性,只要是当前对象的属性返回true,不是的话返回false)

var obj = {name: "王小波", age: 30}; console.log("name" in obj);//->true console.log("height" in obj);//->false

面试题练习

1.写出代码运行的结果;并且把代码运行的步骤写出来;为什么得到所写的结果?

var num = 20;
var obj = {
  num: 30,
  fn: (function (num) {
    this.num *= 3;
    num += 15;
    var num = 45;
    return function () {
      this.num *= 4;
      num += 20;
      console.log(num);
    }
  })(num)//->把全局变量num的值20赋值给了自执行函数的形参,而不是obj下的30,如果想是obj下的30,我们需要写obj.num
};
var fn = obj.fn;
fn();//->65
obj.fn();//->85
console.log(window.num, obj.num);//->240、120

2.写出代码运行步骤

function fo(){
  var i=0;
  return function (n) {
    return n+i++;
  }
};
var f=fo();
var a=f(15);
var b=fo()(15);
var c=fo()(20);
var d=f(20);
console.log(a,b,c,d);//15 15 20 21
  1. 写出输出的结果
	  var ary = [1,2,3,4];
	  function fn(ary){
	    ary[0] = 0;
	    ary = [0];
	    ary[0] = 100;
	    return ary;
	  }
	  var res = fn(ary);
	  console.log(ary);
	  console.log(res);
	  
解析步骤如下;
	  var ary = [1,2,3,4];
	  function fn(ary){
	    ary[0] = 0;//ary=[0,2,3,4] 这个时候修改了变量ary[0]了;ary[0] = 0;这个没有重写变量ary;参数ary只是代表了变量ary,保存的还是变量ary的地址;参数ary[0]指向的还是变量ary[0],也就是1,只是把1改为了0;
	    ary = [0];//ary=[0],这里是重写参数ary了,等号右边的数据[0]保存在fn执行时候产生的堆内存中,参数ary指向的地址,由原来的变量ary地址,重写指向到数据[0]的保存地址,此时的参数ary已经与原来ary无关系了;两者保存的分别是不同的内存地址;
	    ary[0] = 100;//ary=[100]
	    return ary;//[100]
	  }
	  var res = fn(ary);
	  console.log(ary);// [0,2,3,4]
	  console.log(res);//[100]

图解原理如下;


作用域和闭包

作用域和闭包

ECMAScript5;js的代码没有代码块;使用函数运行的机制进行创建闭包;闭包就是作用域的意思;ES5中,JS中只有函数才可以创建能操作的作用域;

JavaScript中的内存也分为栈内存和堆内存。一般来说,栈内存中存放的是存储对象的地址,而堆内存中存放的是存储对象的具体内容。对于原始类型的值而言,其地址和具体内容都存在与栈内存中;而基于引用类型的值,其地址存在栈内存,其具体内容存在堆内存中。堆内存与栈内存是有区别的,栈内存运行效率比堆内存高,空间相对推内存来说较小,反之则是堆内存的特点。所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

  • 函数执行时形成私有作用域
  • 全局变量和私有变量
  • 如何区分函数中出现的变量是私有的还是非私有的
  • 如何查找当前作用域的上级作用域?
  • JavaScript中的顶级作用域是谁?
  • 变量和属性有什么关系
  • 函数运行产生的作用域
  • 闭包
  • 作用域不销毁的情况|内存释放
函数执行时形成私有作用域

函数执行的时候(直接目的:让函数体中的代码执行)会形成一个新的私有的作用域(栈内存),供函数体中的代码执行;

  • 1)给形参赋值
  • 2)私有作用域下的预解释
  • 3)私有作用域下的代码执行

形成的新的私有的作用域还保护了里面的私有变量不受外界的影响,我们把函数的这种保护机制叫"闭包":为什么要有作用域;因为变量要规定活动范围的,为便于管理不同范围的变量;所以要给变量设置活动范围;

预解释也是在各自的作用域里进行预解释的;

function fn(){
	var a=1;
}
fn();
fn();
console.log(a);//Uncaught ReferenceError: a is not defined;
因为a没有声明和定义过,所以报错了;

上面的fn运行了两次,所以产生了两个堆内存;两个作用域(作用域也就是闭包)各自分别有一个a的变量;a的值都是数字1,但是两个变量是不相等的;两个a之间是没有任何关系的;

就好比我们都属于人类;我们都继承了人类这个对象所具有的特征;我有一双手,你也有一双手,但是我们两个人的手是没有关系的;我的手不等于你的手;

全局变量和私有变量
  • 在全局作用域下声明的变量是全局变量;
  • 在私有作用域中声明的变量是私有变量;函数的形参也是私有的变量;
如何区分函数中出现的变量是私有的还是非私有的?

首先看是否为形参,然后看是否在私有作用域中声明过(有没有var过),两者有其一就是私有的变量,那么在当前函数中不管什么位置出现都是私有的,和全局的没有半毛钱的关系;如果两者都没有,说明不是私有的;如果一个函数中出现的变量不是私有的,那么会往其上级作用域查找,上级没有则继续查找,一直找到window为止,如果window也没有呢?

如果是获取:会报错
function fn() {
	console.log(num);//Uncaught ReferenceError: num is not defined
}
fn();
如果是设置:不是私有的,找全局,全局没有的话相当于给全局加一个
function fn() {
	num = 13;//相当于给window增加了一个叫做num的属性名,属性值是13 window.num=13;
}
fn();
console.log(num);//13
如何查找当前作用域的上级作用域?

当前作用域的上级作用域是谁和函数在哪执行的没有任何的关系,我们只需要看当前函数是在哪个作用域下定义的,那么它的上级作用域就是谁;

下面是查找上一级作用域,一直找到window的案例;

var total = 100;
function fn() {
	var total = 10;
	return function () {
		console.log(total);
	}//如果返回的是一个引用数据类型的值(对象、数组、函数...),首先是把这个值开辟一个内存空间,有一个地址,然后把内存地址返回 ->例如在这里其实返回的就是 return xxxfff111;
}
var f = fn();//f=xxxfff111;
f();

//输出的结果是 10 还是 100 ? 为什么 ?

如果是在自执行函数里呢?

var total = 100;
function fn() {
	var total = 10;
	return function () {
		console.log(total);
	}
}
var f = fn();//f=xxxfff111;
~function () {
  var total = 1200;
  f();//->;里面变量的上级作用域是谁
}();

下面代码输出的是什么?

var a=0;
var b="0";
function fn(){
    console.log(a);
  console.log(b);
  console.log(typeof a);
    console.log(typeof b);
  a="1";
  var b=1;
  console.log(a);
  console.log(b);
  console.log(typeof a);
  console.log(typeof b);
}
fn();//运行后会输出什么?如果没有这行代码;函数fn的定义是没有意义的;函数只声明定义,而不运行是没有任何意义的;
//0,undefined,"number","undefined","1",1,"string","number"

预解释是作用域中的预解释;js里是可以函数里面套函数的;都运行的时候,是在函数创建的作用域里再创建一个作用域;

下面是作用域的范围;
var a="window";
console.log(a);//window
function father(){
  console.log(a);//undefined
  var a="father";
  console.log(a);//father
  function children(){
      console.log(b);//undefined
      console.log(a);//father
      var b="flag";
      a="children";
      console.log(a);//children
   }
   children();
   console.log(a);//children
}
father();
console.log(a);//window

 JavaScript中的代码执行顺序是从上到下逐条运行的;遇到function定义函数的代码块;直接跳过;遇到函数执行的代码;就找到引用的函数地址;开始跳到执行函数产生的作用域中执行代码;等函数执行完以后,再回到当前作用域执行下面的代码;

 上面的代码运行后,输出的是

"window",undefined,"father",undefined,"father","children","children","window"

作用域链查找:当作用域套作用域的时候,children内找不到某个变量;会到children的父作用域father中找;当father中找不到的时候;会到father的父作用域找;一直找到window这个根作用域;属于作用域链式查找;

JavaScript中不带var声明变量,变量会冒泡到上一级作用域中;也就是说会污染到上一级作用域;但是对上上级的作用域是没有影响的;很多教材和视频上说不带var声明的变量;变量会变成全局变量,这个说法是不全面的;

【带var和不带var的区别和关系】

只有在全局作用域下才有这样的关系

区别:带var的可以在代码执行前进行声明,而不带var的不能提前的声明;例子如下;

//预解释:var num1;
console.log(num1);//->undefined
var num1 = 12;
console.log(num2);//->Error:num2 is not defined
num2 = 12;

关系:在全局作用域下,var num1=12,不仅仅是增加了一个全局变量,而且还相当于给window增加了一个叫做num1的属性名 -> window.num1=12;

var num1 = 12;
console.log(num1);//12
console.log(window.num1);//12

num2 = 13;
console.log(num2);
console.log(window.num2); //我们可以把window.省略;
下面是函数内带var和不带var的;
console.log(total); //undefined
var total = 0;
function fn() {
  console.log(total); //undefined
 total += 100;
  var total = 200;//total是被保护的;
 console.log(total); //200
}
fn();
console.log(total); //0

下面是不带var的

console.log(total);//undefined
var total = 0;
function fn(num1, num2) {
    console.log(total);//0 ->total不是私有的,找全局下的total,也就是在这里出现的所有的total其实应该都是全局的
    total = num1 + num2;//->全局的total=300
    console.log(total);//300
}
fn(100, 200);
console.log(total);//300

JavaScript中的顶级作用域是谁?

顶级作用域是Global;数据引用高程三133页;

目前只有nodeJs这个语言可以访问到global;nodeJs中是进程;叫process;后台编程中是不管浏览器的事的,node中是global;JavaScript中只可以访问到window;

window相当于global的代理人;好比上帝和教皇的关系;一般在JS中说的全局变量都是window;全局变量就是window的属性;通过window.varName可以访问到;

但并不是所有的都属于window的,比如Object,Math,String,Function等这6个包装类,就不属于window的属性;和window是无关的;类似于中国党政军的关系;互相独立的;

window只是前端编程里的顶级作用域;

变量和属性有什么关系

全局变量是window的属性;window的属性也可以看做全局变量;一个对象的属性,也可以看做以这个对象为作用域下的变量with语句;

  如下:

上面的也可以改造成这样的;[也是闭包的应用]


for (i = 0; i < oPs.length; i++) {
	oPs[i].onclick = function (i) {
		return function(){console.log(i);}
	}(i)
}

高程三上说一个函数里面返回一个函数,这样是才叫闭包;(个人认为闭包是一种机制;是一个封闭的作用域;只有形成封闭作用域都可以叫做闭包的);上面就是高程上说的标准闭包写法;

但是闭包的性能其实没有自定义属性好;可以用自定义属性来写;下面是用自定义属性的写法来做的;

for (i = 0; i < oPs.length; i++) {
	oPs[i].index=i;
	oPs[i].onclick = function () {
		console.log(this.index);
	}
}
关于作用域/闭包的其它理解;
var name = "china";
var age = 5000;
(function (name, age) {//形参变量也是私有作用域中的私有变量
  name = "zhu";
  age = 17;
  alert("zz");
  console.log(name, age);
})(name, age);//执行函数传递参数值,它只是把全局的name和age的值分别的赋值给我们的形参 ->"执行的时候传递的永远是一个值"
alert("22")
console.log(name, age);//先执行自执行函数,然后再执行此行

在函数里面修改全局的name和age(前提是私有的name和age依然存在)

var name = "china";
var age = 5000;
(function (name, age) {
    name = "zhu";
    age = 17;
    //把全局的也修改为"zhu"、7 ->在私有作用域中通过指定前缀来强行的修改的全局下的值
    window.name = name;
    window.age = age;
})(name, age);
console.log(name, age);//zhu ,17 

jQuery部分实现的原理:在私有中定义jQuery,通过window.xxx让其在全局下也可以使用

(function () {
    var jQuery = function () {
      console.log("核心代码");
    };
    window.jQuery = window.$ = jQuery;
})();
jQuery();
$();
闭包用在选项卡等效果上
	var banner = document.getElementById("banner");
	var tip = document.getElementById("tip");
	var oDivs = banner.getElementsByTagName("div");
	var oLis = tip.getElementsByTagName("li");
	//鼠标滑过某一个li,我们让所有的li和所有的div都没有选中的样式(select),然后让当前选中的这个li和对应的div有选中的样式
	
	function changeTab(nIndex) {
	  for (var i = 0; i < oLis.length; i++) {
	   oLis[i].className = null;
	   oDivs[i].className = null;
	  }
	  oLis[nIndex].className = "select";
	  oDivs[nIndex].className = "select";
	}

1、传统的循环绑定事件的方式

for (var i = 0; i < oLis.length; i++) {
    oLis[i].onmouseover = function () {
        changeTab(i);
    };
}

  > 这种方式不能实现我们的效果,为什么?   > 我们是循环所有的li,给每一个li都绑定了鼠标滑过的事件->

/*
i=0
oLis[0].onmouseover=function(){changeTab(i);}; 我们绑定事件其实是把匿名函数定义的主体赋值给元素的事件行为 -> oLis[0].onmouseover=xxxfff000;
i=1
oLis[1].onmouseover=function(){changeTab(i);};
...
i=4
oLis[4].onmouseover=function(){changeTab(i);};
i=5
循环结束
*/

当我们手动触发的这些行为的时候,比如触发第一个li的onmouseover,执行第一个绑定的方法:行程一个私有的作用域->形参赋值->预解释->代码执行,遇到了一个变量i,不是自己的私有的,找全局作用域下的i,这个i的值已经变为循环结束后的5,所以不是我们期望的索引值...

2、如何解决这个问题?

[作用域式解决]

当触发onmouseover的时候,全局的i变为了5,我们找全局的肯定不行,但是我可一这样来操作:

在当前私有作用域和全局作用域之间在嵌入一层作用域A,然后让A中有一个私有的变量i,存储的是自己的索引即可

for (var i = 0; i < oLis.length; i++) {
    (function (i) {
        oLis[i].onmouseover = function () {
            changeTab(i);
        };
    })(i);//每一次循环把当前i的值(0,1,2,3,4)赋值给形参i
}

函数执行形成不销毁的私有的作用域的第二种情况:

在一个函数中,给函数外面的DOM元素绑定事件 -> 由于给事件绑定的方法是一个引用数据类型值,而元素不属于这个作用域,而是函数外面的变量,这样也相当于外面的东西占用了函数里面的内存,当前的私有作用域不销毁

for (var i = 0; i < oLis.length; i++) {
    oLis[i].onmouseover = function (i) {
        return function () {
            changeTab(i);
        };
    }(i);
}

弊端:会形成多个不销毁的私有的作用域,占内存

[自定义属性的方式]

自定义属性处理问题是JS中最常用的编程思想

for (var i = 0; i < oLis.length; i++) {
	oLis[i].index = i;//每一次循环把当前li的索引存储到自己的自定义属性index上,后期需要用到索引的时候我们只需要把自定义属性index的值获取到拿来用即可
	oLis[i].onmouseover = function () {
		//this->当前鼠标滑过的这个li
		changeTab(this.index);
	};
}

案例2:用setTimeout实现把p相对上一个p向右移动100px;1秒中只实现一个P的移动;

setTimeout本身是同步的代码,但是setTimeout里面的函数是异步的代码;setTimeout告诉浏览器隔多少秒执行里面的函数;这个函数是脱离执行顺序的;属于异步代码; 代码如下:

var oDiv = document.getElementById("div1");
var oPs = oDiv.getElementsByTagName("p");
for (i = 0; i < oPs.length; i++) {
	(function(i){
		setTimeout(function(){
			oPs[i].style.left=100*i+"px";
		},i*1000)
	})(i);
}

因为定时器里面的函数是异步的,而setTimeout本身是同步的;所以i和里面的i不一致;用一个闭包把他们包起来;然后传i进去;或者直接包住setTimeout里面的函数;

setTimeout((function(i){
	return function(){oPs[i].style.left=100*i+"px";}
})(i),i*1000);

this关键字

this关键字

  • this和content的区别
  • 如何区分this呢

研究的this都是指[函数执行中的this]

this和content的区别

JS中this是执行的主体(谁来执行的this就是谁),context是执行所在的范围(在哪执行的上下文就是谁) -> "this和context没有必然的关系"

例如:张三在沙县吃饭,this->张三 context->沙县小吃;张三和沙县小吃没有必然关系;
   function 吃饭(){
        this->张三
    }
    张三.吃饭();
    ~function(){
        张三.吃饭();
    }();

如何的区分this呢

this:当前函数执行的主体,它和函数在哪定义的或者在哪执行的都没有任何关系;如何的区分this呢? 1、函数执行,首先看函数名前面是否有".",有的话,"."前面是谁this就是谁;没有的话this就是window 2、自执行函数中的this永远是window,(window去执行的,很多时候window可以省略) 3、给元素的某一个事件绑定方法,当事件触发的时候,执行对应的方法,方法中的this是当前的元素;比如选项卡的代码中;

function fn() {
	console.log(this);
}
var obj = {fn: fn};
fn();//this->window
obj.fn();//this->obj
事件执行,不同的写法,this指向是不同的
document.getElementById("div1").onclick = fn;//fn中的this是#div1
document.getElementById("div1").onclick = function () {
  fn();//this->window
};
function fn() {
  console.log(this);
}
不同作用域的
function sum() {
  console.log("sum",this);//this->window
  fn();//this->window
}
sum();
var oo = {
    sum: function () {
      console.log("oo.sum",this);//this->oo
      fn();//this->window
    }
};
oo.sum();
function fn() {
  console.log(this);
}

你以为 你以为的 就是你以为的


投票的实现原理

投票的实现原理

要求:实现点击一下,加一个数字;

扩展:支持和反对的都有,支持就是累加1,反对就是减1;不小于0;

问题: 同意否?

我非常赞同以上的观点 0

var oBtn = document.getElementById("btn"); var spanNum = document.getElementById("spanNum"); oBtn.onclick = function () { var count = 0; count++; spanNum.innerHTML = count; };

不行,每一次点击div的时候都会重新的执行我们的function,重新的形成一个私有的作用域,从新的开始预解释,代码执行,并且执行完成后当前的作用域还销毁了->每一次count都是从0开始的
1、利用全局作用域不销毁的原理,我们把需要累加的变量count定义为全局变量实现数字的一直累加
	var count = 0;
	oBtn.onclick = function () {
	   count++;
	   spanNum.innerHTML = count;
	};
弊端:我们在真实的项目中为了避免全局变量相互之间的冲突,我们一般是减少(禁用)全局变量的使用的
2、我们自己创造一个不销毁的私有的作用域来存储count

自执行函数执行,形成一个私有的作用域A,A中返回了一个堆内存的地址,被A外面的onclick接收了,那么A这个私有的作用域就不在销毁了,也就是里面的私有变量count也不再销毁了,这样的话count就可以一直累加下去了...

oBtn.onclick = (function () {
    var count = 0;
    return function () {
        count++;
        spanNum.innerHTML = count;
    }
})();
//或者写成下面的也可以
(function () {
    var count = 0;
    oBtn.onclick = function () {
        count++;
        spanNum.innerHTML = count;
    }
})();
弊端:有一个不销毁的私有的作用域还是浪费性能的
3、使用自定义属性(推荐使用)

把需要累加的count存放在当前元素的自定义属性上,并且每一次点击的时候实现累加

var oBtn = document.getElementById("btn");
var spanNum = document.getElementById("spanNum");
oBtn.count = 0;
oBtn.onclick = function () {
  spanNum.innerHTML = ++this.count;
};
4、使用innerHTML的方式

innerHTML:是元素的一个属性,用来存储或者设置元素里面内容的(获取的内容都是一个字符串)

oBtn.onclick = function () {
    var oldNum = spanNum.innerHTML;
    //下面两个都可以
    // spanNum.innerHTML = Number(oldNum) + 1;
    spanNum.innerHTML++;
}

垃圾回收机制

垃圾回收机制

JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存;

在编写 JavaScript 程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

  • 局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使

用这些变量,直至函数执行结束。

  • 此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。在这种情况下,很容易判断变量是否还有存在的必要;
  • 但并非所有情况下都这么容易就能得出结论。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略;标记清除和引用计数。

  • 标记清除(浏览器实现)
  • 引用计数(浏览器实现)
  • 性能问题
  • 开发过程中怎样管理内存(代码实现)
  • 总结

标记清除(浏览器实现)

JavaScript 中最常用的垃圾收集方式是标记清除。

  • 当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
  • 而当变量离开环境时,则将其标记为“离开环境”。

可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。

  • 1、垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
  • 2、它会去掉环境中的变量以及被环境中的变量引用的变量的标记。
  • 3、在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
  • 4、最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

引用计数(浏览器实现)

另一种不太常见的垃圾收集策略叫做引用计数。

引用计数的含义是跟踪记录每个值被引用的次数。

  • 当声明了一个变量并将一个引用类型值赋给该变量时,则这个引用数据值的引用次数就是 1。
  • 如果同一个值又被赋给另一个变量,则该值的引用次数加 1。
  • 相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。
  • 当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
  • 这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

这种方式有一个严重的问题;循环引用;

function problem(){
	var objectA = new Object();
	var objectB = new Object();
	objectA.someOtherObject = objectB;
	objectB.anotherObject = objectA;
}
  • objectA 和 objectB 通过各自的属性相互引用;也就是说,这两个对象的引用次数都是 2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题。
  • 但在采用引用计数策略的实现中,当函数执行完毕后, objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。
  • 假如这个函数被重复多次调用,就会导致大量内存得不到回收。

性能问题;

垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。说到垃圾收集器多长时间运行一次;

不禁让人联想到 IE 因此而声名狼藉的性能问题。IE 的垃圾收集器是根据内存分配量运行的,具体一点说就是 256 个变量、4096 个对象(或数组)字面量和数组元素(slot)或者 64KB 的字符串。达到上述任何一个临界值,垃圾收集器就会运行。这种实现方式的问题在于,如果一个脚本中包含那么多变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量。而这样一来,垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使 IE7 重写了其垃圾收集例程。

随着 IE7 的发布,其 JavaScript 引擎的垃圾收集例程改变了工作方式:触发垃圾收集的变量分配、字面量和(或)数组元素的临界值被调整为动态修正。IE7 中的各项临界值在初始时与 IE6 相等。

  • 如果垃圾收集例程回收的内存分配量低于 15%,则变量、字面量和(或)数组元素的临界值就会加倍。
  • 如果例程回收了 85%的内存分配量,则将各种临界值重置回默认值。
这一看似简单的调整,极大地提升了 IE在运行包含大量 JavaScript 的页面时的性能。

事实上,在有的浏览器中可以触发垃圾收集过程,但我们不建议读者这样做。在IE 中,调用 window.CollectGarbage() 方法会立即执行垃圾收集。在 Opera 7 及更高版本中,调用 window.opera.collect() 也会启动垃圾收集例程。

开发过程中怎样管理内存(代码实现,比较重要的思想)

使用具备垃圾收集机制的语言编写程序,开发人员一般不必操心内存管理的问题。但是,JavaScript在进行内存管理及垃圾收集时面临的问题还是有点与众不同。其中最主要的一个问题,就是分配给 Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全方面的考虑,目的是防止运行 JavaScript 的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。

因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。

这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用;

function createPerson(name){
	var localPerson = new Object();
	localPerson.name = name;
	return localPerson;
}
var globalPerson = createPerson("Nicholas");
// 手工解除 globalPerson 的引用
globalPerson = null;

在这个例子中,变量 globalPerson 取得了 createPerson() 函数返回的值。在 createPerson()函数内部,我们创建了一个对象并将其赋给局部变量 localPerson ,然后又为该对象添加了一个名为name 的属性。最后,当调用这个函数时, localPerson 以函数值的形式返回并赋给全局变量globalPerson 。

由于 localPerson 在 createPerson() 函数执行完毕后就离开了其执行环境,因此无需我们显式地去为它解除引用。

但是对于全局变量 globalPerson 而言,则需要我们在不使用它的时候手工为它解除引用,这也正是上面例子中最后一行代码的目的。

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

总结

JavaScript 变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型的值源自以下 5种基本数据类型: Undefined 、 Null 、 Boolean 、 Number 和 String 。

基本类型值和引用类型值具有以下特点:

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
  • 引用类型的值是对象,保存在堆内存中;
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
  • 确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用instanceof 操作符;如果要绝对准确的判断某个变量的数据类型,可以使用

var testStr = 'cccccccc';
var testAry = [2,3,4,5];
var testObj = {
    name:"zhu",
    age:26,
    gender:"man"
};

console.log({}.toString.call(testStr));//[object String]
console.log({}.toString.call(testAry));//[object Array]
console.log({}.toString.call(testObj));//[object Object]

所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几点总结:

  • 执行环境有全局执行环境(也称为全局环境)和函数执行环境之分;
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
  • 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;
  • 变量的执行环境有助于确定应该何时释放内存。

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。可以对 JavaScript 的垃圾收集例程作如下总结。

  • 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  • “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  • 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法;但在 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种算法仍然可能会导致问题。
  • 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
  • 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。

理解对象

理解对象

面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法;

var person = new Object();//Object就是一个类;person是Object的一个实例;
person.name = "zab";
person.age = 26;
person.job = "WEB";
person.sayName = function(){
    console.log(this.name);
};

上面的例子创建了一个名为 person 的对象,并为它添加了三个属性( name 、 age 和 job )和一个方法( sayName() )。其中, sayName() 方法用于显示 this.name (将被解析为 person.name )的值。

不推荐这么写,推荐字面量的创建方式。前面的例子用对象字面量语法可以写成这样;

var person = {
    name: "zab",
    age: 26,
    job: "WEB",
    sayName: function(){
        console.log(this.name);
    }
};

字面量的写法,可以更像一块;而且传参的时候,可以更直接的看到传的是哪些参数;

ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。” 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。每个对象都是基于一个引用类型创建的,

  • 知识点一、属性类型 defineProperty()
  • 知识点二、定义多个属性(仅作了解) defineProperties()
  • 知识点三、读取属性的特性 getOwnPropertyDescriptor()

知识点一、属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

ECMA-262 第 5 版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。ECMA-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如 [[Enumerable]] 。

数据属性 defineProperty

数据属性指某个数据值是否可以读取和写入值;数据属性有4个描述其行为的特性;

  • [[enumerable]] 是否可枚举 / 是否可读,该特性默认是true;(表示能否通过for-in循环读取到对象的属性; )
  • [[writable]] 是否可写,该特性默认是true;(表示能够修改属性的值)
  • [[configurable]] 是否可删改,该特性默认是true;(表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。)
  • [[value]] 某个属性的属性值;默认是undefined;(读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。)

像前面的例子,他的 Enumerable / Writable / Configurable 默认都是true;name属性的Value被设置为"zab";对该值的任何修改都会在这个属性上反应出来;

修改数据属性:Object.defineProperty(object,name,{})

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是: configurable 、 enumerable 、 writable 和 value 。设置其中的一或多个值,可以修改对应的特性值。

默认的时候
  var person = {
      name: "zab",
      age: 26,
      job: "WEB",
      sayName: function(){
          console.log(this.name);
      }
  };
  console.log(person.name,person.age);
  person.name="zab MODI"
  delete person.age;
  console.log(person.name,person.age);
  for (var key in person) {
    console.log("枚举对象的属性:",key);
  }
使用属性的时候;
  var person = {
      name: "zab",
      age: 26,
      job: "WEB",
      sayName: function(){
          console.log(this.name);
      }
  };
  console.log(person.name);//zab 26
Object.defineProperty(person,"name",{//注意:属性一定要放在字符串中;
  enumerable:false,
  writable:false,
  configurable:false,
  value:"zab1"
});
console.log(person.name);//zab1
person.name="zab MODI"
console.log(person.name);//zab1,person.name="zab MODI"无效;
delete person.name;
console.log(person.name);//zab1 命令delete person.name;无效;

for (var variable in person) {
    console.log("枚举对象的属性,是否有name:",variable);//其它的所有属性都枚举出来了,就是name没有枚举出来;
}

如果把某个特性设置为false,假如再次尝试操作它,在非严格模式下,操作将被忽略(在非严格模式下什么也不会发生);在严格模式下,操作将会导致抛出错误。

而且,一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty() 方法修改除 writable 之外的特性,都会导致错误:

例子:

  var person = {
      name: "zab",
      age: 26,
      job: "WEB",
      sayName: function(){
          console.log(this.name);
      }
  };
  console.log(person.name);//zab
Object.defineProperty(person,"name",{//注意:属性一定要放在字符串中;
  enumerable:false,
  writable:false,
  configurable:false,
  value:"zab1"
});
console.log(person.name);//zab1

Object.defineProperty(person,"name",{//再次设置name会报错
  configurable:true,
  value:"zab2"
});
console.log(person.name);//无法正常打印
在调用 Object.defineProperty() 方法时,如果不指定, configurable 、 enumerable 和writable 特性的默认值都是 false 。多数情况下,可能都没有必要利用 Object.defineProperty()方法提供的这些高级功能。不过,理解这些概念对理解 JavaScript 对象却非常有用。
访问器属性 defineProperty

应用场景:设置一个属性的值会导致其他属性发生变化时。

访问器属性不包含数据值;它们包含一对 getter 和 setter 函数(不过,这两个函数都不是必需的)。 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值; 在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
  • [[Get]] :在读取属性时调用的函数。默认值为 undefined 。
  • [[Set]] :在写入属性时调用的函数。默认值为 undefined 。

访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义。

  var book = {
    _year: 2004,
    age: 1
  };
  Object.defineProperty(book, "year", {
    get: function(){
      return this._year;
    },
    set: function(newValue){
      if (newValue > 2004) {
        this._year = newValue;
        this.age = (newValue - 2004)+1;
      }
    }
  });
  book.year = 2005;
  console.log(book.age); //2

  book.year = 2006;
  console.log(book.age); //3
更改书的时间,会引起书的年限;

以上代码创建了一个 book 对象,并给它定义两个默认的属性: _year 和 age 。 _year 前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。而访问器属性 year 则包含一个getter 函数和一个 setter 函数。getter 函数返回 _year 的值,setter 函数通过计算来确定正确的书籍年龄。因此,把 year 属性修改为 2005 会导致 _year 变成 2005,而 edition 变为 2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。

不一定非要同时指定 getter 和 setter。只指定 getter 意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了 getter 函数的属性会抛出错误。

  var book = {
    _year: 2004,
    age: 1
  };
  Object.defineProperty(book, "year", {
    get: function(){
      return this._year;
    }
  });
  book.year = 2005;
  console.log(book.age); //1

  book.year = 2006;
  console.log(book.age); //1
如果只设置了setter但是不设置getter,在chrome浏览器下可以正常访问; 高程三上说142页,说法有错误,原话是【类似地,只指定 setter 函数的属性也不能读,否则在非严格模式下会返回 undefined ,而在严格模式下会抛出错误。】,亲测可以正常使用,环境chorme 52.0.2743.116 m;
支持 ECMAScript 5 的这个方法的浏览器有 IE9+(IE8 只是部分实现)、Firefox 4+、Safari 5+、Opera12+和 Chrome。在这个方法之前,要创建访问器属性,一般都使用两个非标准的方法:__defineGetter__() 和 __defineSetter__() 。
  var book = {
    _year: 2004,
    age: 0
  };
  // 定义访问器的旧有方法
  book.__defineGetter__("year", function(){
    return this._year;
  });
  book.__defineSetter__("year", function(newValue){
    if (newValue > 2004) {
      this._year = newValue;
      this.age = newValue - 2004;
    }
  });
  book.year = 2005;
  console.log(book.age); //1

知识点二、定义多个属性(仅作了解) defineProperties()

由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.definePro-perties() 方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

  var book = {};
  Object.defineProperties(book, {
    _year: {
      value: 2004
    },
    age: {
      value: 1
    },
    year: {
      get: function(){
        return this._year;
      },
      set: function(newValue){
        if (newValue > 2004) {
          this._year = newValue;
          this.age = newValue - 2004;
        }
      }
    }
  })
以上代码在 book 对象上定义了两个数据属性( _year 和 age )和一个访问器属性( year )。最终的对象与上面中定义的对象相同。唯一的区别是这里的属性都是在同一时间创建的。

支持 Object.defineProperties() 方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。

知识点三、读取属性的特性 getOwnPropertyDescriptor()

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable 、 enumerable 、 get 和 set ;如果是数据属性,这个对象的属性有configurable 、 enumerable 、 writable 和 value

  var book = {};
  Object.defineProperties(book, {
    _year: {
      value: 2004
    },
    age: {
      value: 0
    },
    year: {
      get: function(){
        return this._year;
      },
      set: function(newValue){
        if (newValue > 2004) {
          this._year = newValue;
          this.age = newValue - 2004;
        }
      }
    }
  })


  var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
  console.log(descriptor.value); //2004
  console.log(descriptor.configurable); //false

  console.log(typeof descriptor.get); //"undefined"
  var descriptor = Object.getOwnPropertyDescriptor(book, "year");
  console.log(descriptor.value); //undefined
  console.log(descriptor.enumerable); //false
  console.log(typeof descriptor.get); //"function"

对于数据属性 _year , value 等于最初的值, configurable 是 false ,而 get 等于 undefined 。对于访问器属性 year , value 等于 undefined,enumerable 是 false ,而 get 是一个指向 getter函数的指针。

在 JavaScript 中,可以针对任何对象——包括 DOM 和 BOM 对象,使用 Object.getOwnPropertyDescriptor() 方法。支持这个方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。


创建对象的方式

面向对象的概念:

  • 对象:JS中万物皆对象,它是一个泛指,(类似生活中的世界)
  • 类:对象的具体的细分,(类似生活中的人类,动物类)
  • 实例:某一个类别中具体的一个事物(类似人的个体)
转换成生活中的比喻就是:对象是一个抽象的概念,类似于我们的自然界,世界这种概念;我们自然界中分为了 人类、动物类、植物类...,而我们每一个人都是人类中的一个实例

JS中有一个内置的类 Array(数组类),每一个数组都是这个类的一个实例,我们拿出一个数组(拿出一个实例)来进行研究,研究出来的知识,我们可以说所有的数组都具备这些知识

内置类:

  • Number,String,Boolean,Null,Undefined
  • Object、Array、RegExp、Date、Function...
  • HTMLCollection(元素集合类),通过getElementsByTagName等获取的元素集合都是它的一个实例
  • NodeList(节点集合类),通过getElementsByName等获取的节点集合都是它的一个实例
  • HTMLDivElement(每一个HTML标签都有一个自己对应的类) HTMLElement Element Node EventTarget ...

创建对象的方式

创建对象的方式,最基本的有5种模式:单例模式、工厂模式、构造函数模式、原型模式、构造函数+原型模式;

其它的就是在原型模式,或者构造函数模式的基础上再次改造的模式;

  • 知识点一:单例模式
  • 知识点二:工厂模式
  • 知识点三:构造函数模式
  • 知识点四:原型模式
  • 知识点五:构造函数+原型模式
  • 知识点六:其它扩展模式
- 知识点六.1:动态原型模式(利用原型的动态性)
- 知识点六.2:寄生构造函数模式(构造函数模式+工厂模式)

知识点一:单例模式

单例模式:就是对象类型; Object构造函数或对象字面量都可以用来创建单个对象,下面是字面量的单例模式;(原生的Object构造函创建对象的方式,在某种意义上也是属于单例模式)

var person1 = {
  name: "朱",
  age: 18,
  say:function(){
    console.log("我是person1的say方法");
  }
};
var person2 = {
  name: "安",
  age: 28,
  say:function(){
    console.log("我是person2的say方法");
  }
};
console.dir(person1);//person1是一个单例模式
console.dir(person2);//person2是一个单例模式

把描述同一个事物(同一个对象)的属性和方法放在一个内存空间下,起到了分组的作用,这样不同事物之间的属性即使属性名相同,相互也不会发生冲突

我们把这种分组编写代码的模式叫做"单例模式"

在单例模式中我们把person1或者person2也叫做"命名空间"

单例模式,因为简单易用的原因,是封模块和日常写代码中,最常用的创建方式;

单例模式的优点

直观,易用,对象的可塑性非常强;

单例模式用在模块化开发中

单例模式是一种项目开发中经常使用的模式,项目中使用单例模式来进行"模块化"开发也是很常见的;

"模块化":对于一个相对来说比较大的项目,需要多人协作的开发的,我们一般情况下会根据当前项目的需求划分成几个功能版块,每个人负责一部分,同时开发,在需要用到的某个功能的时候,直接引用该模块即可;简单,易用,拆卸方便;

//公共模块
var utils = {
  select: function () {
    console.log("utils.select");
  }
};

//pageUtilitly的一个主文件
var pageUtilitly = {
    init:function(){
      console.log("现在开始运行bind和select中的代码");
      this.bind();//运行绑定相关的方法;
      this.select();//运行选择相关的方法,如果暂时不需要,直接注释本行代码即可
    },
    bind: function () {
      var self=this;
      self.clickEven();//在自己的命名空间下调用自己命名空间的方法
    },
    select:function(){
      utils.select();//在自己的命名空间下调用其他命名空间的方法
    },
    clickEven:function(){
        console.log("pageUtilitly.clickEven");
    }
};
pageUtilitly.init();
单例模式的缺点

面向对象,创建对象,有点类似做衣服,单例模式的优点是可定制性非常强,有点类似手工做衣服;虽然可塑性非常强,但是缺点就是如果批量做衣服的时候,效率就非常差;如下,tShirt1和tShirt2里面的方法和属性可以随意的改变,可塑性非常强;

var tShirt1 = {
    name: "t-shirt-1",
    size: "xl",
    color: "红",
    hasEffect: function () {
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带装逼效果!");
    }
};
var tShirt2 = {
    name: "t-shirt-2",
    size: "xxxl",
    color: "黑",
    hasEffect: function () {
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带撩妹效果!");
    }
};

tShirt1.hasEffect();
tShirt2.hasEffect();
单例模式虽然解决了分组和创造对象的作用,但是不能实现批量的生产,属于手工作业模式; 假设我要100件tShirt2这种自带撩妹效果的T恤,就需要重复写100次;var tShirt2 这里的代码 这就引申到另外的一种模式:"工厂模式"

知识点二:工厂模式

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

工厂模式是软件工程领域一种广为人知的设计模式,考虑到在 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装特定接口的对象,

工厂模式:说白了就是一种函数类型;

  • 把实现同一件事情的相同的代码放到一个函数中,以后如果在想实现这个功能,不需要从新的编写这些代码来了,只需要执行当前的函数即可
  • "函数的封装" -->"高内聚低耦合":减少页面中的冗余代码,提高代码的重复利用率;(工厂模式的优点就是函数模式的特点)

如下面的例子所示。

function createTshirt(name, size, color){
    var o = new Object();
    o.name = name;
    o.size = size;
    o.color = color;
    o.sayName = function(){
        console.log("这件T恤:" + this.name + "," + this.size + "," + this.color +",自带撩妹效果!");
    };
    return o;
}
var tShirt1 = createTshirt("tShirt1", "xl", "红");
var tShirt2 = createTshirt("tShirt2", "xxxl", "黑");
var tShirt3 = createTshirt("tShirt3", "xxxl", "黑");
var tShirt4 = createTshirt("tShirt4", "xxxl", "黑");

函数 createTshirt() 能够根据接受的参数来构建一个包含所有必要信息的 T恤 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题;

JS是一门轻量级的脚本"编程语言"(HTML+CSS不属于编程语言,属于标记语言)

.net C# php Java c c++ vb vf object-c ....;所有的编程语言都是面向对象开发的->类的继承、封装、多态

继承:子类继承父类中的属性和方法 多态:当前方法的多种形态->后台语言中:多态包含重载和重写
“JS中不存在重载”,方法名一样的话,后面的会把前面的覆盖掉,最后只保留一个 "JS中有一个操作类似重载但是不是重载":我们可以根据传递参数的不一样的,实现不同的功能,属于函数的多态性;
function sum(num) {
    if (typeof num === "undefined") {
        console.log("您没有传参啊,请仔细检查");
        return;
    }
    console.log(num);
}
sum(100);
sum();

#####工厂模式的缺点;

工厂模式虽然解决了重复创建相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型),也就是某个对象属于什么类。随着 JavaScript的发展,又一个新模式出现了。

知识点三:构造函数模式

构造函数模式的意义:构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = function () {
        console.log("my name is " + this.name + ",i can write js 啦!!");
    };
    //浏览器在把创建的实例默认的进行返回,无需return
}
var p1 = new CreateJsPerson("zhu", 18);//CreateJsPerson->this p1  用的时候需要new一下;创造一个构造函数的实例;
var p2 = new CreateJsPerson("an", 28);
p1.writeJs();//writeJs->this p1
console.log(p1);//顺着原型,可以看到他的__proto__指向CreateJsPerson这个构造函数;	
注意:用的时候需要new一下;创造一个构造函数的实例;如果不写new,上面的函数没有任何意义
错误的写法
var res = CreateJsPerson("zhu", 17);//这样写不是构造函数模式执行而是普通的函数执行 由于没有写return所以res=undefined  并且CreateJsPerson这个方法中的this是window
console.log(res);

构造函数模式和工厂模式的区别

  • 1、没有显式地创建对象;
  • 2、直接将属性和方法赋给了 this 对象;
  • 3、没有 return 语句。
  • 4、执行的时候,需要写new
- 普通函数执行->createJsPerson()
- 构造函数模式->new CreateJsPerson()  通过new执行后,我们的CreateJsPerson就是一个类了,而函数执行的返回值(p1)就是CreateJsPerson这个类的一个实例
  • 5、在函数代码执行的时候
- 相同:都是形成一个私有的作用域,然后 形参赋值->预解释->代码从上到下执行 (类和普通函数一样,它也有普通的一面)
- 不同:在代码执行之前,不用自己在手动的创建obj对象了(无显示过程),浏览器会默认的创建一个对象数据类型的值(这个对象其实就是我们当前类的一个实例)
- 接下来代码从上到下执行,以当前的实例为执行的主体(this代表的就是当前的实例),然后分别的把属性名和属性值赋值给当前的实例
- 最后浏览器会默认的把创建的实例返回
  • 6、构造函数模式的函数,推荐首字母大写;
- 按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。

要创建 CreateJsPerson 的新实例,必须使用 new 操作符

会经历四个步骤;

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

在前面例子的最后, p1 和 p2 分别保存着 CreateJsPerson 的一个不同的实例。这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示

var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.constructor == CreateJsPerson); //true
console.log(p1.constructor == CreateJsPerson); //true

原理图如下

下面三种的对比都是true;

console.log(p1.constructor == CreateJsPerson); //true
console.log(p1.constructor == CreateJsPerson); //true

console.log(p1.__proto__.constructor == CreateJsPerson); //true
console.log(p1.__proto__.constructor == CreateJsPerson); //true

console.log(p1.__proto__ == CreateJsPerson.prototype); //true
console.log(p1.__proto__ == CreateJsPerson.prototype); //true
构造函数模式的优点:解决了工厂模式中的归属问题;

对象的 constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型,还是 instanceof 操作符要更可靠一些。我们在这个例子中创建的所有对象既是 Object 的实例,同时也是 CreateJsPerson,这一点通过 instanceof 操作符可以得到验证。

console.log(p1 instanceof CreateJsPerson); //true
console.log(p1.instanceof Object); //true
console.log(p2 instanceof CreateJsPerson); //true
console.log(p2.instanceof Object); //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中, p1 和 p2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object 。

构造函数与其他函数的区别

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = function () {
        console.log("my name is 【" + this.name + "】,i can write js 啦!!");
    };
    //浏览器在把创建的实例默认的进行返回,无需return
}

// 当作构造函数使用
var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);
p1.writeJs(); //"Nicholas"

// 作为普通函数调用
CreateJsPerson("hehe", 27); // 属性和方法都被添加给 window对象了
window.writeJs(); //"hehe"

// 在另一个对象的作用域中调用
var o = new Object();
CreateJsPerson.call(o, "haha", 25);
o.writeJs(); //"haha"

当在全局作用域中调用一个函数时, this 对象总是指向 Global 对象(在浏览器中就是 window 对象)。因此,在调用完函数之后,可以通过 window 对象来调用 writeJs() 方法,并且还返回了 "hehe" 。最后,也可以使用 call() (或者 apply() )在某个特殊对象的作用域中调用 CreateJsPerson() 函数。这里是在对象 o 的作用域中调用的,因此调用后 o 就拥有了所有属性和 writeJs()方法。

构造函数模式的缺点;

p1 和 p2 拥有不同的属性值,但是拥有相同的方法;

var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.writeJs === p2.writeJs);

函数的意义是高内聚,低耦合,避免同样的事情重复创建,所以才把做相同事务的代码封在一起;构造函数中的函数已经失去了函数的意义了;

CreateJsPerson中的writeJs方法,在实例化以后,是不同的;p1.writeJs 并不等于 p2.writeJs;

当然这个问题,也可以通过变种来实现;

function CreateJsPerson(name, age) {
    //浏览器默认创建的对象就是我们的实例p1 -> this
    this.name = name;//->变量p1实例化的时候,那么this就是p1,相当于p1.name=name
    this.age = age;
    this.writeJs = writeJs
    //浏览器在把创建的实例默认的进行返回,无需return
}

function writeJs(){
  console.log("my name is 【" + this.name + "】,i can write js 啦!!");
}

// 当作构造函数使用
var p1 = new CreateJsPerson("zhu", 18);
var p2 = new CreateJsPerson("an", 28);

console.log(p1.writeJs === p2.writeJs);//true

因为writeJs是定义在全局作用域中的,所以p1.writeJs 和 p2.writeJs 的引用地址都是全局中的writeJs,引用地址相等,所以为true;

但是这样做还有一个非常问题,就是CreateJsPerson这个类,依赖writeJs这个方法,两者耦合在一起了,维护起来非常不方便;如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

构造函数的加深理解;
function Fn() {
  this.x = 100;
  this.getX = function () {
    //this->需要看getX执行的时候才知道
    console.log(this.x);
  }
}
var f1 = new Fn;//当f1作为Fn的一个实例的时候,里面的this就是f1
f1.getX();//->方法中的this是f1 ->100
var ss = f1.getX;
ss();//->方法中的this是window ->undefined
  • 1、在构造函数模式中new Fn()执行,如果Fn不需要传递参数的话,后面的小括号可以省略,JS中所有的类都是函数数据类型的,它通过new执行变成了一个类,但是它本身也是一个普通的函数,JS中所有的实例都是对象数据类型的;
  • 2、this的问题:在类中出现的this.xxx=xxx中的this都是当前类的实例,而某一个属性值(方法),方法中的this需要看方法执行的时候,前面是否有"."才能知道this是谁;如果函数只是定义,没有引用,那么里面的this是不知道是谁的,函数只有被引用了,才知道引用被执行时的this指向谁;
  • 3、类有普通函数的一面,当函数执行的时候,var num其实只是当前形成的私有作用域中的私有变量而已,它和我们的f1这个实例没有任何的关系;只有this.xxx=xxx才相当于给f1这个实例增加私有的属相和方法,才和我们的f1有关系...
	function Fn() {
	  var num = 10;
	  this.x = 100;//f1.x=100
	  this.getX = function () {//f1.getX=function...
	    console.log(this.x);
	  }
	}
	var f1 = new Fn;
	console.log(f1.num);//->undefined
  • 4、在构造函数模式中,浏览器会默认的把我们的实例返回(返回的是一个对象数据类型的值);如果我们自己手动写了return返回:返回的是一个基本数据类型的值,当前实例是不变的,例如:return 100; 我们的f1还是当前Fn类的实例;但是返回的如果是一个引用数据类型的值,当前的实例会被自己返回的值给替换掉,例如:return {name:"zhu"} 我们的f1就不在是Fn的实例了,而是对象{name:"zhu"}
function Fn() {
  this.x = 100;
  this.getX = function () {
      console.log(this.x);
  };
  return {name: "zhu"};
}
var f1 = new Fn;
console.log(f1);//此时的f1是{name: "zhu"}
  • 5、检测某一个实例是否属于这个类->instanceof

对于检测数据类型来说,typeof有自己的局限性,不能细分object下的对象、数组、正则...

function Fn() {
    this.x = 100;
    this.getX = function () {
        console.log(this.x);
    };
}
var f1 = new Fn;
console.log(f1 instanceof Array);//->false 说明f1不是一个数组
console.log(f1 instanceof Fn);//->true 说明f1是Fn的一个实例
console.log(f1 instanceof Object);//->true 说明a是一个对象
  • 6、下面f1和f2都是Fn这个类的一个实例,都拥有x和getX两个属性,但是这两个属性是各自的私有的属性,所以
function Fn() {
    this.x = 100;
    this.getX = function () {
        console.log(this.x);
    };
}
var f1 = new Fn;
var f2 = new Fn;
console.log(f1.getX === f2.getX);//->false

知识点四:原型模式


在构造函数的理解图里

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Person prototype"
var person2 = new Person();
person2.sayName(); //"Person prototype"
console.log(person1.sayName == person2.sayName); //true

我们将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName() 函数。

理解原型

  • 节点1. 理解原型对象
  • 节点2. 判断某个属性属于原型,而不是私有属性;
  • 节点3. constructor属性的指向问题
  • 节点4. 原型的动态性
  • 节点5. 原生对象的原型
  • 节点6. 原型模式的问题
节点1. 理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person 。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

总结

  • 1、只要创建了一个新函数Person,系统会自动为该函数创建一个 prototype属性,Person.prototype这个属性指向函数的原型对象。
  • 2、函数的原型对象里自动获得一个constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
  • 3、函数的原型对象里自动获得一个__proto__属性,默认指向Object;
  • 4、Person的实例person1自动获得__proto__属性,默认指向Person.prototype;
  • 5、调用person1的方法或者属性,默认会在自己的内部找;如果找不到,会通过person1.__proto__找到Person.prototype;如果Person.prototype再找不到,会通过Person.prototype.__proto__,一直找到Object这个基类;如果再找不到就会报undefined或者报错;
  • 6、只要创建了一个新函数Person,函数Person本身也会自动获得Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向Object;

具体的对应逻辑代码如下:

function Person007(){
  var name="Person007"
}
console.log(Person007.prototype);//1、只要创建了一个新函数Person,系统会自动为该函数创建一个 prototype属性,Person.prototype这个属性指向函数的原型对象。
console.log(Person007.prototype.constructor);//2、函数的原型对象里自动获得一个constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
console.dir(Person007.prototype.__proto__);//3、函数的原型对象里自动获得一个__proto__属性,默认指向Object;

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();

console.dir(Person.__proto__);//只要创建了一个新函数Person,函数Person本身也会自动获得Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向Object;

person1.sayName(); //Person prototype
person1.__proto__.sayName(); //4、Person的实例person1自动获得__proto__属性,默认指向Person.prototype;
person1.__proto__.__proto__.sayName(); //5、调用person1的方法或者属性,默认会在自己的内部找;如果找不到,会通过person1.__proto__找到Person.prototype;如果Person.prototype再找不到,会通过Person.prototype.__proto__,一直找到Object这个基类;如果再找不到就会报undefined或者报错;
深入理解person1的查找原理;
function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型

person1.name="change name"
console.log(person1.name);//来自实例;

delete person1.name//使用 delete 操作符删除了 person1.name ,之前它保存的 "change name"值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中 name 属性的连接。因此,接下来再调用person1.name 时,返回的就是原型中 name 属性的值了
console.log(person1.name);//来自原型
hasOwnProperty :检测一个属性是不是私有属性(这个方法只在给定属性存在于对象实例中时,才会返回 true )
function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型
console.log(person1.hasOwnProperty("name"));//false

person1.name="change name"
console.log(person1.name);//来自实例
console.log(person1.hasOwnProperty("name"));//true

delete person1.name
console.log(person1.name);//来自原型
console.log(person1.hasOwnProperty("name"));//false

节点2. 判断某个属性属于原型,而不是私有属性

操作符 in:单独使用和在 for-in 循环中使用。在单独使用时, in 操作符会在通过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中。

代码如下:

function Person(){}
Person.prototype.name = "Person prototype";
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();

console.log(person1.name);//来自原型
person1.sayName();//来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符in","name" in person1);//true

person1.name="change name"
console.log(person1.name);//来自实例
console.log(person1.hasOwnProperty("name"));//true
console.log("操作符in","name" in person1);//true

delete person1.name
console.log(person1.name);//来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符in","name" in person1);//true

同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,

function hasPrototypeProperty(object, name){
	return !object.hasOwnProperty(name) && (name in object);
}

由于 in 操作符只要通过对象能够访问到属性就返回 true , hasOwnProperty() 只在属性存在于实例中时才返回 true ,因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false ,就可以确定属性是原型中的属性

节点3. constructor属性的指向问题

每添加一个属性和方法就要敲一遍 Person.prototype 。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示

function Person(){
}
Person.prototype = {
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外: constructor 属性不再指向 Person 了

function Person(){
}
Person.prototype = {
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

一般写了原型后,就会把函数指回他原来的类;

function Person(){
}
Person.prototype = {
    // constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

以这种方式重设 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的

function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};

// 重设构造函数,只适用于 ECMAScript 5  兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);
节点4. 原型的动态性
  • 在某个类函数的原型上追加方法和属性;之前创建的的类函数实例,都可以获得新追加的属性和方法;
  • 当重写某个类函数的原型时,之前创建的类函数实例,无法获得重写后的方法和属性;
function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};
var person1=new Person;
console.log(person1.addTestKey);//undefined
Person.prototype.addTestKey="test value";
console.log(person1.addTestKey);//test value

如果换一种写法,下面这种的,就无法获得重写原型后的属性了;

function Person(){
}
var person1=new Person;
Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
        alert(this.name);
    }
};
console.log(person1.addTestKey);//undefined
Person.prototype.addTestKey="test value";
console.log(person1.addTestKey);//undefined

原理是;因为person1创建的时候,里面的person1.__proto__,指向的是默认的Person.prototype;新改变后的prototype已经改变了;而访问person1.addTestKey的时候,访问的是老地址中的这个属性;

节点5. 原生对象的原型

var testAry=[1,2,3]
console.log(testAry.__proto__==Array.prototype);//true

原生的Array类就是上面的这种思路;testAry这个实例的__proto__,指向数组类的prototype;

也可以通过修改原生对象的原型,来追加方法的;

String.prototype.startsWith = function (text) {
	return this.indexOf(text) == 0;
};
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
console.log(msg.startsWith("world")); //false
节点6. 原型模式的问题
  • 因为原型的动态性,导致实例之间的属性和方法会互相干扰;

所有的属性都是定义在原型上的,所有的实例都会继承原型上的属性和方法;

  function Person(){
  }
  Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    testAry:["test1","test1"]
  };
  var person1=new Person;
  var person2=new Person;

  console.log(person1.testAry,person2.testAry);//思考,为什么此时输出的时候,person1.testAry还会有test-push-person1;而不是["test1","test1"]呢?
  person1.testAry.push("test-push-person1");

改变person1的testAry后,就会影响到person2的testAry的属性;

表现如下

这就是原型模式的弊端

不同的实例之间需要有自己的单独的私有属性和方法,并不能都是老共的!如果每个人的东西都是大家伙的,那就乱套了;上面的构造函数模式中,总结的是构造函数中,每个实例的中的内部属性都是相互独立的,为了解决公用的方法才引入原型模式的概念;这就引申到了公认最佳的创建类的方式构造函数模式+原型模式
思考题:为什么下面person1改变后,并不会影响person的变化;
  function Person(){
  }
  Person.prototype = {
    constructor : Person,
    name : "Person prototype",
    age : 29,
    job: "WEB",
    sayName : function () {
      console.log(this.name);
    }
  };
  var person1=new Person;
  var person2=new Person;

  person1.age+=2;
  person1.sayName=null;

  console.log(person1.age,person1.sayName);
  console.log(person2.age,person2.sayName);

#知识点五:构造函数+原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

  function CreateJsPerson(name, age) {
    this.name = name;
    this.age = age;
    this.like =["one","two"];
  }
  CreateJsPerson.prototype = {
    constructor:CreateJsPerson,
    writeJs:function(){
      console.log("my name is " + this.name + ",i can write js 啦!!");
    }
  };
  var p1 = new CreateJsPerson("zhu1", 18);
  var p2 = new CreateJsPerson("zhu2", 28);
  p1.writeJs();//->my name is zhu1,i can write js 啦!!
  p2.writeJs();//->my name is zhu2,i can write js 啦!!
  p1.like.push("p1-like");
  console.log(p1.like);//["one", "two", "p1-like"]
  console.log(p2.like);//["one", "two"]
  console.log(p1.writeJs === p2.writeJs);//->true
  console.log(p1.like === p2.like);//->false

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 writeJs() 则是在原型中定义的。而修改了 p1.like (向其中添加一个新字符串),并不会影响到 p2.like ,因为它们分别引用了不同的数组。

这种构造函数与原型混成的模式,是目前在 ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

1、构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立开的->实例识别

基于构造函数模式的原型模式解决了方法或者属性公有的问题->把实例之间相同的属性和方法提取成公有的属性和方法 ->想让谁公有就把它放在CreateJsPerson.prototype上即可

  • 1、每一个函数数据类型(普通函数、类)都有一个天生自带的属性:prototype(原型),并且这个属性是一个对象数据类型的值
  • 2、并且在prototype上浏览器天生给它加了一个属性constructor(构造函数),属性值是当前函数(类)本身
  • 3、每一个对象数据类型(普通的对象、实例、prototype...)也天生自带一个属性:__proto__,属性值是当前实例所属类的原型(prototype)
  function Fn() {
    this.x = 100;
    this.sum = function () {
    };
  }
  Fn.prototype.getX = function () {
    console.log(this.x);
  };
  Fn.prototype.sum = function () {
  };
  var f1 = new Fn;
  var f2 = new Fn;
  console.log(Fn.prototype.constructor === Fn);//->true
2、Object是JS中所有对象数据类型的基类(最顶层的类)

  //1)、f1 instanceof Object ->true 因为f1通过__proto__可以向上级查找,不管有多少级,最后总能找到Object

  //2)、在Object.prototype上没有__proto__这个属性

  function Fn() {
    this.x = 100;
    this.sum = function () {
    };
  }
  Fn.prototype.getX = function () {
    console.log(this.x);
  };
  Fn.prototype.sum = function () {
  };
  var f1 = new Fn;
  var f2 = new Fn;
//  console.log(Fn.prototype.constructor === Fn);//->true
  console.log(f1 instanceof  Object);//->true
  console.dir(f1);//->原型,原型链的查找;

##### 3、原型链模式

知识点六:其它扩展模式

一般包括下面三种

- 知识点六.1:动态原型模式(利用原型的动态性)
- 知识点六.2:寄生构造函数模式(构造函数模式+工厂模式)
1.动态原型模式(利用原型的动态性)
应用场景:这是一个在原型模式上的优化方法; 原理:对原型所做的修改,能够立即在所有实例中得到反映,利用了原型的动态性;
运行思路:可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。只有某个方法不存在的情况下,才会将它添加到原型中。if中的代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。有点类似图片延迟加载的思路;分批实现,从而提高脚本的性能;
  function Person(name, age, job){
    //属性
    this.name = name;
    this.age = age;
    this.job = job;
    
    // 方法
    if (typeof this.sayName != "function"){
      Person.prototype.sayName = function(){
        console.log(this.name);
      };
    }
  }
  var friend = new Person("Nicholas", 29, "Software Engineer");
  friend.sayName();

对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。仅仅只是一个判断语句,用什么方法来判断,就根据所在的场景来实现的了;

2.寄生构造函数模式(构造函数模式+工厂模式)

通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。

表现形式:工厂模式的代码;却用构造函数来实例化类;
  function SpecialArray(){
    this.name="SpecialArray";
    this.age="2222";

    var values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function(){
      return this.join("|");
    };
    return values;
  }
  var colors = new SpecialArray("red", "blue", "green");
  console.log(colors);
  console.log(colors.toPipedString()); //"red|blue|green"

构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

    this.name="SpecialArray";
    this.age="2222";

这两行代码在加上return这个语句后,相当于白写了;

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。


继承的几种方法

继承的几种方法

继承是面向对象语言中的一个最为人津津乐道的概念。许多面向对象都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

继承的概念:子类继承父类中的属性和方法; 继承的目的:子类直接使用父类的方法,不需要再重写一遍; 继承的实际应用:B想有一个getX的方法,我们发现A中已经有了,我接下来只需要让B作为儿子,让A作为父亲,让B继承A中的属性和方法;
  • 知识点一:原型继承
  • 知识点二:call继承
  • 知识点三:call加原型继承(推荐)
  • 知识点四:冒充对象继承
  • 知识点五:中间类继承

知识点一、原型继承

原型继承的原理:B要继承A,只需要让B的原型等于A的一个实例即可 ->B.prototype=new A;

function A() {
  this.x = 100;
}
A.prototype.getX = function () {
  console.log(this.x);
};
function B() {
  this.test="test"
}
B.prototype.getY = function () {
  console.log("B.prototype");
};
B.prototype = new A;
var b = new B;
console.dir(b);
b.x=200;
b.getX();//200;
A.prototype.getX();//undefined
//子类重写父类的方法
b.__proto__.__proto__.getX = function () {
    console.log(this.x + 1000);
};
b.getX();//1200;

1)原型继承是我们JS中最常用的一种继承方式,属于改写原生的原型,指向需要继承的实例;;

2)原型继承不是把父类的属性和方法复制一份给自己,而是更改了原型链的查找顺序,我们b想要使用getX这个方法,是通过__proto__一级级的向上查找,最后也找到A的原型上,获取getX并且使用 -> b.__proto__.__proto__.getX=A.prototype.getX

3)子类不仅仅可以获取使用父类上的方法,而且还可以把父类原型上的方法进行修改 b.__proto__.__proto__.getX=function(){console.log(this.x+1000);} 就是把父类A原型上的方法修改了,这样的话,A的其他的实例也会受到影响 ->"这叫做类的多态(是多态中的重写):子类重写父类的方法"

4)在原型继承中,子类可以把父类中私有和公有的属性/方法都继承过来使用;并且都是作为子类B公有的属性/方法;(因为B每次查找都经过A的实例,所以A中的私有方法和属性,会被B所继承)

原型继承的应用;
写一个方法,求评委打分的平均分(去掉一个最高分,去掉一个最低分)
function fn() {
    arguments.__proto__ = Array.prototype;
    arguments.sort(function (a, b) {
        return a - b;
    });

    arguments.shift();//去掉最高分
    arguments.pop();//去掉最低分
    console.log(eval(arguments.join("+")) / arguments.length);
    console.log(arguments.join("+") , arguments.length);
}

fn(56,67,78,89,90,12,23,34 ,45);
fn(56, 67, 78, 89, 90, 12, 23, 34, 45 , 45,14,34,67);
原型继承的缺点:
function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){}

//继承了 SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green,black"
1、完全继承了A,自己原型上的方法被吃掉了;杀敌1000,自损800的做法; 2、最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。(所有属性都是大家伙的了) 3、由第二个问题导致的就是,在创建子类的实例时,不能向父类的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

所以引出了call继承(apply同样的原理);

知识点二、call继承(apply同样的原理,借用构造函数/伪造对象/经典继承)

call继承的原理:在子类B的私有属性中,把父类A当做一个普通的函数执行,并且把里面的this变为子类B的一个实例b,这样的话,在执行A的时候,A中写的this.xxx=xxx都相当于给b增加的私有的属性

function A() {
    this.x = 100;//b.x=100 给b增加一个私有的属性x=100
    this.flagA="flagA"
}
A.prototype.getX = function () {
    console.log(this.x);
};
function B() {
    //this->b
    //A() ->作为一个普通的函数执行,this->window,A的原型是起不到作用的
    A.call(this);//->执行A函数,把A里面的this变为b
    this.flagB="flagB"
}
B.prototype.getBPrototype = function () {
    console.log("B.prototype");
};

var b = new B;
console.log(b);

1)call继承首先只能继承父类A中的私有的属性(this.xxx=xxx),并且和原型不一样,call继承是把A中的私有的属性复制一份直接的给了B的实例

2)把父类A中的私有属性继承到子类B中的私有的属性

b的结构如下;

call继承的优点

对实例1的操作不会影响实例2,

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
// 继承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors); //"red,blue,green"
call继承的缺点
只继承了A的私有属性,没有继承A的原型; 如果仅仅是call继承,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在父类的原型中定义的方法,对子类而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

所以引出了call继承+原型继承;

知识点三、call继承加原型继承(也叫做伪经典继承)

call继承+原型继承

function A() {
    this.x = 100;
    this.flagA="flagA"
}
A.prototype.getX = function () {
    console.log(this.x);
};
function B() {
    A.call(this);
    this.flagB="flagB"
}
B.prototype.getBPrototype = function () {
    console.log("B.prototype");
};


B.prototype = new A;
var b = new B;

console.log(b);

call继承加原型继承解决的事情

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

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
};
function SubType(name, age){
    //继承属性
    SuperType.call(this, name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;//constructor 需要写回去,否则SubType虽然继承了SuperType的私有属性和公有属性,但是SubType原来的原型被重写了;
SubType.prototype.sayAge = function(){
    console.log(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
用这种模式一定要注意原型的constructor重写会子类,否则SubType虽然继承了SuperType的私有属性和公有属性,但是SubType原来的原型被重写了;

知识点四、冒充对象继承(call继承的升级版)

冒充对象继承:在子类B的私有函数体中,创建一个父类A的实例temp(temp中就存有父类A中的私有和公有的属性/方法),我们把temp当做一个普通的对象进行遍历,分别把temp中存有的属性都存储到我们子类B的私有中一份

function A() {
    this.x = 100;
    this.flagA="flagA"
}
A.prototype.getX = function () {
    console.log(this.x);
};
function B() {
    // A.call(this);
    this.flagB="flagB"

    var temp = new A;//->temp中现在就存有了A中的私有和公有的属性/方法
  for (var key in temp) {
      this[key] = temp[key];
  }
  temp = null;
}
B.prototype.getBPrototype = function () {
    console.log("B.prototype");
};

var b = new B;
console.log(b);
  • 1)、冒充对象继承首先继承父类中的私有和公有的属性/方法,并且和原型不一样,冒充对象继承是把A中的私有的属性和共公有的方法复制一份直接的给了B的实例
  • 2)、把父类A中的私有属性和共公有的方法继承到子类B中的私有的属性
备注:temp只是一个借用的对象,所以用完记得置为null,类似数组排序中的借用的那个对象,仅仅只是内部处理所需的,没有实际的意义;

test


JS中的常见算法

JS中的常见算法
  • 插入排序
  • 冒泡排序
  • 快速排序
常见操作数组的
  • 数组去重复
  • 从一个长度10万的数组中,随机取出100条数据(取出的数据不能重复);
JS中的常见算法

插入排序

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

操作步骤:首先选取数组的第一项即ary[0],我们可以认为这个数是已经安排好序的,再取ary[1]项插入到已经排好序的元素中,此时只有ary[0],我们比较ary[0]和ary[1];大于ary[0]就放在后面,小于就插到ary[0]前面;要插入的元素依次为ary[1]至ary[ary.leng-1]项;插入到排序好的数组中的时候,插入每一项都需要从后面往前面遍历已经排序好的元素;

如果排序好的元素比插入的元素大,则把该元素往后挪一位,知道已经排序的元素小于等于要插入的元素(或者已经遍历完已经排好序的数组项),则把要插入的元素放在该位置+1的索引位置中(反向排的时候,需要放在数组的第0个位置),对每个插入的元素都执行上面的操作,最终数组就是排序好的;

有BUG的写法;

    var ary=[11,2,31,45,6,78,37,33,21];
    function insertSort(ary){
        var temp;//定义一个临时变量,保存要插入的值;
        for(var i= 1,len=ary.length;i<len;i++){
            if(ary[i]<ary[i-1]){
                temp=ary[i];
                ary[i]=ary[i-1];
                ary[i-1]=temp;
            }
        }
        return ary;
    }
    var newAry=insertSort(ary);
    console.log(newAry);//[2, 11, 31, 6, 45, 37, 33, 21, 78]

因为只把小的数往前移动了一次;这里需要用到while循环,把6移动2后面;

    var ary=[11,2,31,45,6,78,37,33,21];
    function insertSort(ary){
        var temp;//定义一个临时变量,保存要插入的值;
        for(var i= 1,len=ary.length;i<len;i++){
            if(ary[i]<ary[i-1]){
                temp=ary[i];//需要插入的值;
                var pIndex=i-1;//需要插入值的前一个索引;
                while(temp<ary[pIndex]&&pIndex>=0){
                    ary[pIndex+1]=ary[pIndex];//相当于ary[i]=ary[i-1];
                    ary[pIndex]=temp;//相当于ary[i-1]=temp;完成一波交换;
                    pIndex--;//准备下一波交换;
                }
            }
        }
        return ary;
    }
    var newAry=insertSort(ary);
    console.log(newAry);//[2, 6, 11, 21, 31, 33, 37, 45, 78]

冒泡排序

基本思想:就是把数据像泡泡一样网上冒,让体积最轻的泡泡浮在最上面,然后按照重量往下依次排列;

冒泡排序的操作原理:

    var ary=[11,2,31,45,6,78,37,33,21];

用循环的方式,循环数组的长度,然后比较数组的数据

  • 第一次循环把ary的最大值放在ary[ary.length-1]的位置;
- ary[0]>ary[1],如果是true,说明第一项比第二项大,交换ary[0]和ary[1]的位置;如果是false则不改变位置;
- ary[1]>ary[2],如果是true,说明第二项比第三项大,交换ary[1]和ary[2]的位置;如果是false则不改变位置;(此时的ary[1]肯定比ary[0]大的,因为开始的时候已经把ary[0]和ary[1]比较了)
- ……
- ary[ary.length-2]>ary[ary.length-1];比较到这个时候,ary[ary.length-1]的位置肯定是最大的数据了;
  • 第二次循环,把倒数第二大的数据,放在数组的ary[ary.length-2]位置;
- 备注:第二次循环的时候,只需要比较到ary[ary.length-2]的位置就可以了,因为ary[ary.length-1]已经是最大值了,无需再次比较;
注意:交换位置的时候,需要一个中间变量来协助,假设让a和b进行数据交换; var temp=a;a=b;b=temp; 这样就可以把a和b的数据交换好了;
    var ary=[11,2,31,45,6,78,37,33,21];
    function sortAry(ary){
        var aryLen=ary.length;//获取数组的长度;有aryLen个数在排序;
        var temp=null;//临时变量,交换数据中用的
        for(var i= 0,iLen=aryLen-1;i<iLen;i++){//外层循环n-1次;
            for(var j= 0,jLen=aryLen-1-i;j<jLen;j++){//每次循环完,都能从剩下的数组中找出个最大的数组放在aryLen-1-i的位置;
                if(ary[j]>ary[j+1]){
                    temp=ary[j];
                    ary[j]=ary[j+1];
                    ary[j+1]=temp;
                }
            }
        }
        return ary;
    }
    var newAry=sortAry(ary);
    console.log(newAry);//[2, 6, 11, 21, 31, 33, 37, 45, 78]
避免无效循环的方法;
    var ary=[11,2,31,45,6,78,37,33,21];
    function sortAry(ary){
        var aryLen=ary.length;//获取数组的长度;有aryLen个数在排序;
        var temp=null;//临时变量,交换数据中用的
        var flag=false;//设置标志位,初始化为false
        for(var i= 0,iLen=aryLen-1;i<iLen;i++){//外层循环n-1次;
            for(var j= 0,jLen=aryLen-1-i;j<jLen;j++){//每次循环完,都能从剩下的数组中找出个最大的数组放在aryLen-1-i的位置;
                if(ary[j]>ary[j+1]){
                    temp=ary[j];
                    ary[j]=ary[j+1];
                    ary[j+1]=temp;
                    flag=true;//如果交换好了,做个标记,避免无效的循环;
                }
            }
            if(!!flag){//只要交换了位置,flag的值就重新设置为false了;
                flag=false;
            }else{//如果没有交换,说明数组已经排好序了,可以结束循环了;
                break;
            }
        }
        return ary;
    }
    var newAry=sortAry(ary);
    console.log(newAry);//[2, 6, 11, 21, 31, 33, 37, 45, 78]

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

算法步骤:

  • 1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 3)针对所有的元素重复以上的步骤,除了最后一个。
  • 4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

快速排序;

基本思想:首先从数组ary选取一个基准点(通常我们取中间项作为基准点),然后遍历数组,把小于基准项放到基准点左边集合,把大于基准项放在基准点右边集合;然后对左边和右边两个集合分别再重复前面的操作;(直到每个集合就剩下一个元素的时候),其实就是一个递归的思想;

操作步骤:

    var ary=[11,2,31,45,6,78,37,33,21];

依旧拿上边这个数组做例子,,我们确定一个基准点,

    var ary=[11,2,31,45,6,78,37,33,21];
    var pivot=ary[Math.floor(ary.length/2)];
    console.log(pivot);//6

然后我们循环数据中的剩余项,把小于6的放在数组left中,把大于等于56的数组项放在数组right中,第一轮操作结束后left=[2],right=[11,31,45,78,37,33,21]

然后对left重复以上的操作,直到left和right仅剩一项或者空时候结束,最终返回left+pivot+right;

    var ary=[11,2,31,45,6,78,37,33,21];
    function quickSort(ary){
        var aryLen=ary.length;//获取ary的长度;
        if(aryLen<=1){//如果ary小于等于1的长度,就直接返回了;这一步非常重要,是判断是否循环完的标志;
            return ary;
        }
        var pINdex=Math.floor(ary.length/2);//基准项的所引值
        var pivot=ary.splice(pINdex,1);//删除基准项,并把基准项赋值给pivot;
        var left=[],right=[];
        for(var i= 0,len=ary.length;i<len;i++){
            if(ary[i]<pivot[0]){
                left.push(ary[i]);
            }else{
                right.push(ary[i])
            }
        }
        return quickSort(left).concat(pivot,quickSort(right));
    }
    var newAry=quickSort(ary);
    console.log(newAry);//[2, 6, 11, 21, 31, 33, 37, 45, 78]
常见操作数组的

数组去重;

  • 第一种方法:每一次拿出数组中的当前项和后面的所有项进行比较,如果相同的话,删除即可;
    var ary=[1,2,34,56,6,7,45,3,2,2,12,4,9,8,6,7,4];
    function deleteRepeat(ary){
        for(var i= 0,len=ary.length-1;i<len;i++){//比较length-1次;
            var temp=ary[i];
            for(var j=i+1;j<ary.length;j++){
                if(temp==ary[j]){
                    ary.splice(j,1);
                    j--;//删除以后需要把j重置下,避免数组塌陷;
                }
            }
        }
        return ary;
    }
    var newAry=deleteRepeat(ary);
    console.log(newAry);
这种处理方法,处理少量数据的时候没有问题的,但是做很多数组的时候,性能就是一个很大的问题了,双重循环毕竟还是有优化的空间的,通常用第二种方法;

第二种方法:利用对象的属性名和属性值不能重复的原理,把数组中每一项的值当作对象的属性名属性值进行储存,在每一次储存之前进行判断,如果对象中已经存在这一项,我们就删除数组中这项,不存在我们就把这一项储存到我们的新的对象中,(注意对象在这里只是一个临时储存的作用),在处理完成后最好置为null;养成好习惯 ;

    var ary=[1,2,34,56,6,7,45,3,2,2,12,4,9,8,6,7,4];
    function deleteRepeat(ary){
        var obj={};
        for(var i= 0,len=ary.length;i<len;i++){
            var temp=ary[i];
            if(obj[temp]==temp&&temp!=undefined){
                ary.splice(i,1);
                i--;
                continue;
            }
            obj[temp]=temp;
        }
        obj=null;
        return ary;
    }
    var newAry=deleteRepeat(ary);
    console.log(newAry);

从一个长度10万的数组中,随机取出100条数据(取出的数据不能重复);

原理:随机获取100个索引,每一次获取的时候,都把对应的项获取,为了避免数组中的项重复,每获取一个就把原有数组中的这一项删除;

为了方便掩饰从20个里取5个随机数;

第一种比较耗性能的写法;

    var ary=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
    function selectOption(ary,optionNumber){
        var targetAry=[];
        if(!optionNumber){
            console.log("请指定需要删选的数量!");
        }
        for(var i=0;i<optionNumber;i++){
            var ran=Math.round(Math.random()*(ary.length-1));//获取0-数组最大索引的一个随机数;
            targetAry.push(ary[ran]);//按照获取的随机索引,把对应的值取出来,放到新书组中;
            ary.splice(ran,1);//数组中实现删除的时候比较耗性能,为了牵扯到数组长度的改变,数组被删除的项,后面所有数据都前进一位;
        }
        return targetAry;
    }
    var newAry=selectOption(ary,2);
    console.log(newAry);
确定在代码后面已经说明,这种的比较耗性能;
    var ary=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
    function selectOption(ary,optionNumber){
        var targetAry=[];
        if(!optionNumber){
            console.log("请指定需要删选的数量!");
        }
        for(var i=0;i<optionNumber;i++){
            var ran=Math.round(Math.random()*(ary.length-1));//获取0-数组最大索引的一个随机数;
            targetAry.push(ary[ran]);//按照获取的随机索引,把对应的值取出来,放到新书组中;
            ary[ran]=ary[ary.length-1];//把数组最后一项放到当前要删除索引的这一项;
            ary.length=ary.length-1;//删除最后一项;
        }
        return targetAry;
    }
    var newAry=selectOption(ary,10);
    console.log(newAry);
这种的写法,是操作数组的常用处理方式;借助数组length可修改的性能;

DOM面向对象

DOM面向对象

,低版本IE 中的所有 DOM 对象都是以 COM对象的形式实现的。这意味着 IE 中的DOM 对象与原生 JavaScript 对象的行为或活动特点并不一致。这些差异需要注意。

  • DOM节点的层次
  • Node类型
  • Document类型
  • Element类型
  • Text类型
  • DocumentFragment类型
  • Attr类型

DOM节点的层次

DOM是树形的结构,

<!doctype html>
<html>
<head>
    
    <title>Document</title>
    
</head>
<body>
    <p>hello word</p>
    
</body>
</html>

DOM的结构如下

节点间的各种关系可以用传统的家族关系来描述,相当于把文档树比喻成家谱。在 HTML 中,可以将 元素看成是 元素的子元素;相应地,也就可以将 元素看成是 元素的父元素。而 元素,则可以看成是 元素的同胞元素,因为它们都是同一个父元素 的直接子元素。

Node类型

DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。节点分为几种不同的类型,每种类型分别表示文档中不同的信息及(或)标记。每个节点都拥有各自的特点、数据和方法,另外也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形结构

文档节点只有一个子节点,即 元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文 档只能有一个文档元素。在 HTML 页面中,文档元素始终都是 元素。在 XML 中,没有预定义的元素,因此任何元素都可能成为文档元素。

每一段标记都可以通过树中的一个节点来表示:HTML 元素通过元素节点表示,特性(attribute)通过特性节点表示,文档类型通过文档类型节点表示,而注释则通过注释节点表示。总共有 12 种节点类型(nodeType),这些类型都继承自一个基类型。

hello word

hello word

hello word

hello word

可以输出看下Node上面的属性和方法;

重点关心下面几个属性;

  • nodeType 节点类型
  • nodeName 节点名称
  • nodeValue 节点值
  • childNodes 获取所有的子节点
  • children 获取所有的元素子节点
  • firstChild 获取第一个子节点(不一定是元素节点)
  • lastChild 获取最后一个子节点
  • nextSibling 获取下一个弟弟节点(不一定是元素节点)
  • previousSibling 获取上一个哥哥节点
  • parentNode 获取父亲节点

Node上面的方法

  • appendChild 将节点添加到指定容器的最后面
  • insertBefore 将新的节点添加到指定容器中某一个节点的前面
  • replaceChild 把原有的元素进行替换
  • removeChild 删除元素
  • cloneNode 复制原有的节点,如果传递的值是true,代表将子子孙孙的后代元素一起赋值,false代表只复制当前元素本身
  • hasChildNodes 返回一个布尔值,表明当前节点是否包含有子节点.
  • normalize

DOM1 级定义了一个 Node 接口,该接口将由 DOM 中的所有节点类型实现。这个 Node 接口在JavaScript 中是作为 Node 类型实现的;除了 IE 之外,在其他所有浏览器中都可以访问到这个类型。JavaScript 中的所有节点类型都继承自 Node 类型,因此所有节点类型都共享着相同的基本属性和方法。

Node的属性

nodeType / nodeName / nodeValue

NodeType 有以下几种;但是并不需要都记住的;

  • ELEMENT_NODE:1
  • ATTRIBUTE_NODE:2
  • TEXT_NODE:3
  • CDATA_SECTION_NODE:4
  • ENTITY_REFERENCE_NODE:5
  • ENTITY_NODE:6
  • PROCESSING_INSTRUCTION_NODE:7
  • COMMENT_NODE:8
  • DOCUMENT_NODE:9
  • DOCUMENT_TYPE_NODE:10
  • DOCUMENT_FRAGMENT_NODE:11
  • NOTATION_NODE:12

在JS中常用的只有几个,元素节点,文本节点,注释节点,document,他们对应的值是

名次      数字  nodeName的值    nodeValue的值
元素节点    1   大写的标签名      null
文本节点    3   #text           文本内容
注释节点    8   #comment        注释内容
document   9   #document       null
如果四个还记住,记住一个元素节点就可以了;(元素节点 ELEMENT_NODE:1,)

在标准浏览器中会把空格,Enter当作文本节点;

if (someNode.nodeType == 1){ // 适用于所有浏览器,注意不要用Node.ELEMENT_NODE进行比较,在低版本IE中不兼容;
    console.log("Node is an element.");
}

nodeName 和 nodeValue 这两个属性。这两个属性的值完全取决于节点的类型。在使用这两个值以前,最好是像下面这样先检测一下节点的类型。

if (someNode.nodeType == 1){
    testNodeName = someNode.nodeName; //nodeName 的值是元素的标签名
    testNodeValue = someNode.nodeValue; //nodeValue 的值是null
}

在这个例子中,首先检测节点类型,看它是不是一个元素,如果是则取得并保存nodeName的值,对于元素节点nodeName中保存的始终都是元素的标签名,而nodeValue的值则始终为null;

childNodes

hello word1

hello word2

hello word3

hello word4

每个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。 NodeList 是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来访问 NodeList 的值,而且这个对象也有 length 属性,但它并不是 Array 的实例。

上面例子展示了如何访问保存在 NodeList 中的节点——可以通过方括号,也可以使用 item()方法 , 个人喜欢用方括号。

注意 length 属性表示的是访问 NodeList 的那一刻,其中包含的节点数量,如果改变之后,再次访问它的length,会看到变化。
    //检测数据类型
    console.log({}.toString.call(selectAll));//[object HTMLCollection]
    //类数组转为真正的数组
    var arrayOfNodes = Array.prototype.slice.call(selectAll);
    console.log(arrayOfNodes);
    console.log({}.toString.call(arrayOfNodes));//[object Array]

上面的方法IE678不兼容,

    //类数组转为真正的数组,兼容写法
    function convertToArray(nodes){
        var array = [];
        try {
            array = Array.prototype.slice.call(nodes, 0); //针对非 IE 浏览器
        } catch (e) {
            for (var i= 0,len=nodes.length; i < len; i++){
                array.push(nodes[i]);
            }
        }
        return array;
    }
    var arrayOfNodes = convertToArray(selectAll);
    console.log({}.toString.call(arrayOfNodes));//[object Array]

childNodes需要注意的(元素的子节点方法;)

看下面的代码

  • Item 1
  • Item 2
  • Item 3
  • Item 1
  • Item 2
  • Item 3

myList1的childNodes是7,myList2的chilNodes是3;(myList1里面有Text 类型)

如果在myList1中获取全部的li,可以直接用getElementByTagName;

var test1=document.getElementById("myList1");
var lis=test1.getElementsByTagName("li");
console.log(lis);

这样就拿到了,先确定范围,然后通过标签名来获取;还可以判断元素的是否为原来来获取

var test1=document.getElementById("myList1");
var ary=[];
for(var i=0,len=test1.childNodes.length;i<len;i++){
    if(test1.childNodes[i].nodeType===1){
        ary.push(test1.childNodes[i]);
    }
}
console.log(ary);

Text 类型的介绍

<!-- 没有内容,也就没有文本节点 -->
<div></div>
<!-- 有空格,因而有一个文本节点 -->
<div> </div>
<!-- 有内容,因而有一个文本节点 -->
<div>Hello World!</div>

上面代码给出的第一个

元素没有内容,因此也就不存在文本节点。开始与结束标签之间只要存在内容,就会创建一个文本节点。因此,第二个
元素中虽然只包含一个空格,但仍然有一个文本子节点;文本节点的 nodeValue 值是一个空格。第三个
也有一个文本节点,其 nodeValue 的值为 "Hello World!" 。

parentNode、firstNode、lastNode、nextSibling、previousSibling

DOM结构如下

hello word1

hello word2

hello word3

hello word4

总结:

  • 1、父节点的 firstChild 和 lastChild属性分别指向其 childNodes 列表中的第一个和最后一个节点。firstChild等于childNodes[0],lastChild原理一样;
  • 2、在只有一个子节点的情况下, firstChild 和lastChild 指向同一个节点。如果没有子节点,那么 firstChild 和 lastChild 的值均为 null 。
  • 3、如果列表中只有一个节点,那么该节点的 nextSibling 和 previousSibling 都为 null

Node的方法

  • appendChild
  • insertBefore
  • replaceChild
  • cloneNode
  • hasChildNodes
appendChild

将节点添加到指定容器的最后面,返回新增的节点。

hello word1

hello word2

hello word3

hello word4

参考:https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild

insertBefore

将新的节点添加到指定容器中某一个节点的前面,方法接受两个参数:要插入的节点和作为参照的节点。

	var insertedNode = parentNode.insertBefore(newNode, referenceNode);

插入节点后,被插入的节点会变成参照节点的前一个同胞节点( previousSibling ),同时被方法返回。

如果参照节点是null ,则 insertBefore() 与 appendChild() 执行相同的操作

https://developer.mozilla.org/en-US/docs/Web/API/Node/insertbefore

replaceChild

把原有的元素进行替换

replacedNode = parentNode.replaceChild(newChild, oldChild);
  • 如果newChild已经存在于DOM树中,首先会被删除;
  • oldChild会被newChild所取代,olcChild是被替换掉的原始节点
  • replacedNode
cloneNode

赋值节点

var dupNode = node.cloneNode(blooeanValue);
  • node是被赋值的节点
  • dupNode是赋值后的文件;
  • blooeanValue 是否采用深度克隆
- 如果为true,则该节点的所有后代节点也都会被克隆
- 如果为false,则只克隆该节点本身.
  • 虽然 blooeanValue是可选的参数,但是推荐当做必须参数用;

备注:

  • 克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如onclick="alert(1)"),但不会拷贝那些使addEventListener()方法或者node.onclick = fn这种用JavaScript动态绑定的事件.
- 推荐:平时写代码的时候,用事件委托的方式去写代码;
  • clonenode()可能导致文件重复的元素ID,如果原始节点设置了ID,并且克隆节点会被插入到相同的文档中,那么应该更新克隆节点的ID以保证唯一性。name属性可能也需要进行修改
  • 想要克隆一个节点来添加到另外一个文档中,请使用Document.importNode()代替本方法.
removeChild

该方法从指定的DOM结构中,删除一个子元素;返回值是要删除的节点;

hello word1

hello word2

hello word3

hello word4

  • oTest1和parent1是父子节点的关系;
  • returnValue===oTest1,
  • 如果要删除的和被删除的节点不是父子关系,会报错
- 子元素不存在:parameter 1 is not of type 'Node'.
- 要删除的子元素,不是父元素内的子元素:The node to be removed is not a child of this node.

参考:https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild

hasChildNodes

返回一个布尔值,表明当前节点是否包含有子节点.

语法:element.hasChildNodes() 注意:Node.hasChildNodes是个方法,而不是普通属性,使用时必须加括号才能调用.
var foo = document.getElementById("foo");
if ( foo.hasChildNodes() ) { 
  foo.removeChild( foo.childNodes[0] );
}

如果id为foo的这个元素有子节点,则从dom树中删除它的第一个子节点.

Document类型

  • 查找元素
  • 特殊集合
  • 文档写入
  • 文档的子节点
  • 文档信息
  • DOM一致性检测

输出document,查看有哪些属性和方法;

    console.dir(document);

查找元素

查看 Document 上的方法,最常用的就是查找元素的方法了;document的意思是整个页面的

console.log(document)

输出看一下;

  • getElementById
  • getElementsByClassName
  • getElementsByName
  • getElementsByTagName
  • querySelector
  • querySelectorAll
document.getElementById()

在整个文档中,通过元素的ID获取这个元素的对象,如果页面中没有这个ID,则获取的内容为null;

需要注意的

getElementById这个方法的写法,不能改变大小写; document.getElementById("id"); 前面的范围,必须是document; id是大小写敏感的字符串,代表了所要查找的元素的唯一ID.element是一个元素的,一个,找到的也是第一次找到id的所在元素; getElementById方法不会搜索不在文档中的元素。当创建一个元素,并且分配ID后,你必须要使用insertBefore或其他类似的方法把元素插入到文档中之后才能使用 getElementById 获取到:如果不插入文档中,属于游离状态,此时获取是获取不到的;
    var element = document.createElement("div");
    element.id = 'testId';
    console.log(document.getElementById('testId'));//null
    document.body.appendChild(element);
    console.log(document.getElementById('testId'));//<div id="testId"></div>

hello word1

hello word2

hello word3

hello word4

document.getElementsByClassName()

在整个文档中,通过元素class样式类的值来获取一组元素,IE678不兼容

var elements = document.getElementsByClassName(names); 
// or:
var elements = rootElement.getElementsByClassName(names);
  • elements 是复数,多个class的意思,查找到的所有元素的集合 HTMLCollection .返回的是一个类数组,如果找到某一个class,可以借助前面的范围,并且[0],这样的方式来确定某一个元素;
  • names 是一个字符串,表示用于匹配的 class 名称列表; 多个 class 名称通过空格分隔
  • getElementsByClassName 可以在任意的元素上调用,不仅仅是 document。 调用这个方法的元素将作为本次查找的根元素(范围);
document.getElementsByTagName()

在整个文档中,通过元素的标签名来获取一组元素(HTMLcollection集合),获取的是一个类数组,获取的个数可以通过length获取,获取集合的某一个元素,通过对象的索引即可查找;

即使一个文档里面,只有一个元素集合;这个元素也需要用[0]的方式给选出来,否则操作的是一个类数组;

document.getElementsByName()

在整个文档中,通过元素的name值来获取一组元素(NodeList集合),在IE浏览器中,此方法只对表单元素起作用;

hello word1

hello word2

hello word3

hello word4
document.querySelector() 在整个文档中,通过不同的选择器获取一个元素对象,IE678不兼容
document.querySelectorAll() 在整个文档中,通过不同的选择器获取一组元素对象集合,IE678不兼容;
document.documentElement 获取整个HTML对象
document.body 获取整个body对象

特殊集合

除了属性和方法,document对象还有一个特殊的集合,这些集合都是HTMLCollection对象,为了访问文档常用的部分提供的快捷方式;

正常工作中,很少直接这么用的;

文档写入

将输出流写入到网页中,有4个方法;

  • write(string) 原样写入
  • writeIn(string) 在字符串的末尾添加一个换行符(\n);
  • open()
  • close()

write和writeIn

<!doctype html>
<htm>
<head>
    
    <title>Document</title>
    
</head>
<body>
<p>now time is:</p>
</body>
</htm>

open()和close()分别用于打开和关闭网页的输出流;如果是加载期间使用write()或者writeIn()方法,则不需要用到这两个方法;

文档的子节点

  • document.documentElement;//拿到这个标签;
  • document.body//拿到body这个层的标签;

主要就是这两个方法的,当然也可以用别的方法来获取同样的效果的;

比如

document.childNodes[0];
document.firstChild;

这两个都可以获取到html标签;

文档信息

  • document.title 设置文档的标题
  • document.URL 获取或者设置完整的URL
  • document.domain 获取或者设置域名
  • document.referrer 取得来源页面的URL

DOM一致性检测

  • hasFeartrue()

Element类型

  • HTML元素
  • 操作特性
  • attributes属性
  • 创建元素

Element和Document类型一样,都是非常使用的类型;

Element的类型,nodeType的值为1、nodeName为元素的标签名,nodeValu是null;如果要访问元素的标签名,可以使用nodeName属性,也可以使用tagName属性;两个属性返回相同的值;

这是一个ID为test的div内容

返回的标签名是大写的DIV;一般比较都会转成小写然后比较;如下;

if(test.tagName.toLowerCase()=="div"){
    console.log("目标的元素是div标签,开始执行代码");
}

HTML元素

所有的HTML元素都是由HTMLElement类型表示,如果不是直接通过这个类型,也是通过它的子类型表示;可以输出看下,HTMlElement类型继承自ELement类型的;

这是一个ID为test的div内容

输出的图片的如下;

每个HTML元素都存在下面的标准特性

  • id 元素在文档中的唯一标识符
  • title 有关元素的附加说明信息,鼠标放上去会显示,(以前我以为只有a标签才有,其实是都有的)
  • lang 元素内容的语言代码,很少使用
  • dir 语言的方向,值为ltr或者rtl,很少使用(left-to-right和right-to-left);rtl可以起到右对齐的效果;
  • className 与元素的class特性对应,是元素指定的CSS类,没有将这个属性命名为class,是因为class是ECMAscript的保留字;【*】
这是一个ID为test的div内容!!!ddddd.

这是一段文字

操作特性

  • getAttribute()
  • setAttribute()
  • removeAttribute()

getAttribute:

var test=document.getElementById("test");
console.log(test.getAttribute("id"));//test
console.log(test.getAttribute("className"));//null
console.log(test.getAttribute("class"));//test-class-name
console.log(test.getAttribute("title"));//鼠标放上来的提示
console.log(test.getAttribute("lang"));//en
console.log(test.getAttribute("dir"));//rtl(最后一个字符不能是符号,否则符号在前面显示)
console.log(test.getAttribute("diy-data"));//hehe

这个属性要注意的是,如果想获取到属性,必须先取得这个属性所在的元素;然后再进行相关的Attribute操作;这个属性可以获得自定义属性,根据HTML5规范,自定义的特性应该加上data-前缀进行验证;

转给getAttribute的特性名,与实际的特性名相同,因此要想得到class特性值,应该传入class而不是calssName;

但是有两类特殊的特性,虽然有对应的属性名,但是属性的值与通过getAttribute返回的不同;

  • 1、style
  • 2、onclick
<!doctype html>
<htm>
<head>
    
    <title>Document</title>
</head>
    
<body>
<div style="color: #999;" id="test">这是一段测试文字;</div>

</body>
</htm>

通过JS操作DOM的时候,一般不适应getAttribute,而是只使用对象的属性,在取得自定义特性质的情况下,才会使用getAttribute的方法;

setAttribute:

接受两个参数,第一个参数是特性名,第二个参数是特性值;如果特性已经存在,setAttribute会以指定的值替换现有的值;如果没有的值会创建并且新增;

通过这个方法设置的特性名会被统一转换为小写的形式;"ID"会最终变成id,"CLASS"会被变为"class";

var test=document.getElementById("test");
test.setAttribute("CLASS","dddddd");//class="dddddd"

removeAttribute:

彻底删除元素的特性,调用这个方法不仅清除特性的值,而且也会从元素中完全删除特性;(IE6不支持)

创建元素,

document.creatElement()方法,可以创建新元素,接受一个参数,就是要创建的元素,参数可以是标签名

  • div

一般都使用标签名;

创建的元素属于游离状态,还没有被添加到文档树中,因为新建的元素并不会马上看到,可以使用appendChild,inserBefore,replaceChild()方法,

var test=document.getElementById("test");
var creatEle=document.createElement('p');
creatEle.id="test-id";
creatEle.className="class-test";
creatEle.innerHTML="innerHTML";
test.appendChild(creatEle);

借助createDocumentFragment创建元素

文档碎片;document fragment在文档没有对应的标记,为了赋值大量数据渲染时的一种节约性能方法;

原理是:创建大量元素的时候,把元素都先放在文档碎片里,等元素创建添加完,把文档碎片统一插入到指定的元素中;

类似与我们生活中,发快递的时候,快递公司通过三轮车把所有的件搜收集起来,然后再统一拉回仓库去发件;

代码如下:

    Text类型的方法

    • document.createTextNode() 创建新文本节点
    • normalize() 相邻文本节点合并
    • splitText() 分割文本节点

    document.createTextNode()

    创建新文本节点,接收一个参数,要插入节点中的文本,(作为参数的文本将按照HTML或者XML的格式进行编码)

    var ele=document.createElement("div");
    var textNode=document.createTextNode('<strong>hello</strong> word');
    var anotherTextNode = document.createTextNode("lee!");
    ele.className="message";
    ele.appendChild(textNode);
    ele.appendChild(anotherTextNode);
    document.body.appendChild(ele);

    如果两个文本节点是相邻的同胞节点,那么这两个节点中的文本就会连起来显示,中间不会有空格。

    normalize

    如果在一个包含两个或多个文本节点的父元素上调用 normalize() 方法,则会将所有文本节点合并成一个节点,结果节点的 nodeValue 等于将合并前每个文本节点的 nodeValue 值拼接起来的值。

    var ele=document.createElement("div");
    var textNode=document.createTextNode('<strong>hello</strong> word');
    var anotherTextNode = document.createTextNode("lee!");
    ele.className="message";
    ele.appendChild(textNode);
    ele.appendChild(anotherTextNode);
    document.body.appendChild(ele);
    console.log("childNodes的长度:"+ele.childNodes.length+",第一个节点值是"+ele.firstChild.nodeValue);//childNodes的长度:2,第一个节点值是<strong>hello</strong> word
    ele.normalize();
    console.log("childNodes的长度:"+ele.childNodes.length+",第一个节点值是"+ele.firstChild.nodeValue);//childNodes的长度:1,第一个节点值是<strong>hello</strong> wordlee!

    splitText()

    和normalize()相反的方法;按照指定的位置分割 nodeValue 值。

    • 原来的文本节点将包含从开始到指定位置之前的内容;(不包含所传的索引)
    • 新文本节点将包含剩下的文本。
    var ele=document.createElement("div");
    var textNode=document.createTextNode('hello word');
    ele.className="message";
    ele.appendChild(textNode);
    document.body.appendChild(ele);
    console.log("childNodes的长度:"+ele.childNodes.length+",第一个节点值是"+ele.firstChild.nodeValue);//childNodes的长度:2,第一个节点值是<strong>hello</strong> word
    var newNode=ele.firstChild.splitText(2);
    console.log(ele.childNodes.length);//2
    console.log(ele.firstChild.nodeValue);//he
    console.log(newNode.nodeValue);//llo word

    精华总结

    • tagName 获取元素标签的名称
    • className 获取或者设置元素class属性的值
    - <div class="test1"></div>
    - 可以通过className获取test1,也可以把这个值进行修改;
    - oDiv.className="test2",此时div的calss就变为test2了,注意,这里的修改会覆盖原来的所有属性;
    • style 获取或者设置元素的行内样式,在获取的时候要注意,只能获取行内上写的样式,非行内样式不能获取;
    - oDiv.style.backgroundColor="red";就是设置元素的行内北京颜色是红色;
    • window.getComputerStyle 获取元素所有经过计算的样式(只要在页面中显示出来,那么该元素的样式都是经过计算的)这个方法,不兼容,一般是下面这种处理
    - window.getComputerStyle?window.getComputedStyle(ele,null)[attr]:ele.currenStyle[attr]
    • document.creatElement() 创建一个元素节点,例如document.creatElement("div")就是创建一个div元素
    • document.creatDocumentFragment() 创建文档碎片

    封装DOM库

    #第三次封装DOM库 这个是我第三次封装DOM库了;

    第一次封装的DOM库

    第二次封装的DOM库

    前面两次都封装的不满意;这次改进之前我还是感觉很满意的。但是封装好以后,我又感觉还是该继续升级了!主要是没有做好报错处理,

    参考(兼容性处理、错误处理与调试)

    打算把这个做底层方法来用的,下次升级,打算直接在控制台输出什么方法,因为什么原因错误的;如果test方法的传参应该是数组,结果传了字符串;那么后台报出test的参数错误的了,如下

    throw new Error("test():arguments must array");//手动抛出错误信息介绍,调用这个函数的时候,如果错了,一下就知道问题了;

    控制台上显示:test():arguments must array,搭眼一看就知道了,增加开发效率;

    后期打算把封装的好的DON库,动画库、事件库、AJAX库,重构一下;组成一个基础类库,然后再这个基础库上,再封一个brostool的模块化处理的方法库;按照W3C写的代码为HTML基础;

    假设要做选项卡的效果;导入brostool.js文件后,获取选项卡的ID;然后撸一串for循环,绑定个事件;就完事了;

    实际需要代码数量如下;

    1. for(var i= 0,len=oLi.length;i
    2. this.index=i;
    3. oLi[i].onmouseover=brosTools.changeTab;
    4. }

    封装常用的效果后,加起班来,根本停不下来!

    当然,这种思路的实现,我已经做出来了,只是还没有正式封装;

    源码和DEMO可以点这里:模块化开发的DEMO演示:


    这次封的DOM思路和以前一样,封在Tool.prototype上的;在tool里装了一个属性;

    this.flag = “getElementsByClassName” in document;

    这个在IE678是不兼容的;如果这个是true说明是标准浏览器;如果是false,说明是IE大爷;

    下面是封装的风格;

    var Tool = function () {//构造函数模式;用的时候需要new一下;
        this.flag = "getElementsByClassName" in document;
        //getElementsByClassName 在IE678中是不存在的。用这个来判断是不是低版本的IE浏览器;
        //每次只需要判断this.flag是否存在就可以了;如果存在就是标准浏览器,如果不存在就是IE;
    };
    Tool.prototype = {//方法是定义在Tool的prototype上的;
        constructor: Tool,//重写prototype后,prototype的constructor已经不是原来的Tool了;需要手动给他强制写会到Tool上去;
        getIndex: function () {},//简单的备注说明;
        toJSON:function(){},//简单的备注说明;
        likeArray:function(){}//简单的备注说明;
    }
    封装的函数兼容到IE6;

    受jQuery的链式写法的启发,我用了函数调用的封装好的方法;好比获取上一个哥哥节点是getPre;当封装所有哥哥们的时候,就借用封号好的getPre;DOM库内的getPres之类的封装就是这种思想封的;

    在作用域套作用域的时候;子作用域内尽量不返回引用数据类型,因为闭包内的值,是另外一个子闭包的返回值的时候,如果子闭包的返回值是字面量,那么浏览器会在空闲的时候,把作用域销毁;而如果返回值的是一个引用数据类型的值,那么闭包是不会销毁的,在性能优化上,不好!

    1、如何调用

    tool.js 使用的是构造函数模式开发的类库,使用的时候需要创建一个实例,例如:

    var t=new Tool;

    t.getElementsByClassName("context","className");

    2、提供了那些的方法

    (1)getElementsByClassName

    作用:在指定的上下文中根据元素的类名(class样式名)获取对应的元素,旨在解决getElementsByClassName在IE6~8下不兼容;

    参数:[className] 样式名称-->"string";

    [context] 上下文

    返回值:我们获取的元素集合-->"Array"

    (2)setGroupCss

    t.setGroupCss(oDiv,{left:100, top:20,opacity:0.3});

    封装的方法预览

    • getElementsByClassName :处理低版本IE的兼容性问题;
    • toJSON :JSON格式的字符串转成JSON对象;
    • isType :判断数据类型
    • listToArray :类数组转为数组;(arguments)
    • 下面是设置DOM的方法
    • getEleChildren :获取元素下面的指定的标签名的元素集合;
    • getFirst :第一个元素节点
    • getLast :最后一个元素节点
    • getPre :相邻哥哥的元素节点
    • getPres:相邻哥哥们的元素节点
    • getNext:相邻弟弟的元素节点
    • getNexts:相邻弟弟们的元素节点
    • getSibling:上一个哥哥和下一个弟弟
    • getSiblings:所有的兄弟们(除了自己)
    • getIndex:获取索引值
    • insertAfter:在指定目标元素oldEle后面插入新元素newEle;如果没有传指定元素,则直接添加在后面;这个方法好像没啥实际意义;可以通过insertBefore变相实现;
    • prependChild :把一个元素节点添加为parentEle的第一个子节点;
    • innerHTML :获取元素的innerHTML;
    • text :获取和设置innerHTML;
    • 下面是设置CSS
    • setCss :设置CSS属性值和获取CSS;如果三个参数就是设置,2个参数就是获取;
    • setGroupCss:给元素设置一组属性;cssObj是一个对象类型;
    • offset ://获取偏移量;
    • hasClassName:判断是否有某个className;
    • addClassName :增加某个className;
    • removeClassName :移除类样式的方法;
    • 在DOM库上增加方法,同时不影响原来的方法;在类的原型上增加方法;
    • aryPro.removeRepeat :数组去重;
    • aryPro.forEachPro :forEach兼容性处理;
    • objPro.myHasPubProperty :是否为公有属性;
    • strPro.myTrim :去除首尾空格;
    • strPro.mySub :是不是有效的
    • strPro.myFormatTime :时间格式化;
    • strPro.myQueryURLParameter :是否是有效URL

    3、扩展(不修改原有的类但是可以使用原有类的方法)

    自定义类实现继承我们的Tool类:

    例如:

    1)原型链继承

    • var Fn=function(){};
    • Fn.prototype=new Tool;
    • var f=new Fn;

    f就可以调用我们的Tool上面的方法了;因为f是Fn的一个实例了,而且f会通过;f.__proto__也就是Fn.prototype;找到Tools的函数; 扩展的时候,也可以在f上直接f.XXX来扩展;


    正则基础

    正则表达式的基础

    • 正则是什么
    • 如何创建一个正则
    • 如何学习正则
    • 正则的转义

    1、正则是什么?

    • 正则:它就是一个规则,用来处理字符串的一个规则(正则就是用来处理字符串的)
    • 原理:
    - 1)匹配  判断一个字符串是否符合我们制定的规则->test:reg.test(str)
    	var reg=/\d+/;
    	console.log(reg.test("朱"));//false
    	console.log(reg.test("1"));//true
    	console.log(reg.test(1));//true
    	console.log(reg.test("朱朱朱111"));//true
    - 2)捕获  把字符串中符合我们正则规则的内容捕获到->exec:reg.exec(str)
    	var reg=/\d+/;
    	console.log(reg.exec("朱"));//null
    	console.log(reg.exec("1"));//["1", index: 0, input: "1"]
    	console.log(reg.exec("朱朱朱111"));//["111", index: 3, input: "朱朱朱111"]
    	正则捕获,如果比配,返回捕获匹配到了数据;如果不匹配,返回null;

    2、如何创建一个正则?

    • 字面量方式:
    var reg = /\d/;
    • 实例创建方式:
    var reg = new RegExp("");

    正则的两种创建方式是有区别的

      在字面量方式中,我们//之间包起来的所有的内容都是元字符,有的具有特殊的意义,大部分都是代表本身含义的普通的元字符

      var name = "zhuan";
      var reg = /^\d+"+name+"\d+$/g;
      console.log(reg.test("2015zhuan2016"));//->false
      console.log(reg.test('"2015"""zhuan2016""'));//->false
      console.log(reg.test('2015"""nameeeee"2016'));//->true

    对于这样的需求,我们只能使用实例创建的方式了

    	var name = "zhuan";
    	// var reg = /^\d+"+name+"\d+$/g;
    	var reg = new RegExp("^\\d+" + name + "\\d+$", "g");
    	console.log(reg.test("2015zhuan2016"));//->true
    	console.log(reg.test('2015"""nameeeee"2016'));//->false

    字面量方式和实例创建的方式在正则中的区别?

    • 1)字面量方式中出现的一切都是元字符,所以不能进行变量值的拼接,而实例创建的正则表达式可以使用变量;
    • 2)字面量方式中直接写\d就可以,而在实例中需要把它转译 \\d
    要注意的是,传递给 RegExp 构造函数的两个参数都是字符串(不能把正则表达式字面量传递给 RegExp 构造函数)。由于 RegExp 构造

    函数的模式参数是字符串,所以在某些情况下要对字符进行双重转义。所有元字符都必须双重转义,那些已经转义过的字符也是如此,例如 \n (字符\在字符串中通常被转义为\\,而在正则表达式字符串中就 会变成\\\\)。

    3、如何学习正则?

    var name = "zhuan";
    // var reg = /^\d+"+name+"\d+$/g;
    var reg = new RegExp("^\\d+" + name + "\\d+$", "g");
    
    console.log(reg);
    console.dir(reg);//顺着原型链查找有哪些方法
    console.dir(RegExp.prototype);//正则类的原型

    4、正则转义

    与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:

    ( [ { \ ^ $ | ) ? * + .]}

    这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义;

    //匹配第一个"bat"或"cat",不区分大小写
    var pattern1 = /[bc]at/i;
    console.log(pattern1.test("bat"));      //true
    console.log(pattern1.test("cat"));      //true
    console.log(pattern1.test("[bc]at"));   //false
    //匹配第一个" [bc]at",不区分大小写
    var pattern2 = /\[bc\]at/i;
    console.log(pattern2.test("bat"));      //false
    console.log(pattern2.test("cat"));      //false
    console.log(pattern2.test("[bc]at"));   //true
    //匹配所有以"at"结尾的 3 个字符的组合,不区分大小写
    var pattern3 = /.at/gi;//注意index的变化
    console.log(pattern3.exec("[bc]at"));   //["]at", index: 3, input: "[bc]at"]
    console.log(pattern3.exec("bat"));      //null
    console.log(pattern3.exec("cat"));      //["cat", index: 0, input: "cat"]
    //匹配所有".at",不区分大小写
    var pattern4 = /\.at/gi; 

    正则的组成

    正则的组成

    var reg = /\d/;//->包含一个0-9之间的数字
    console.log(reg.test("zhu"));//->false
    console.log(reg.test("zhu1"));//->true
    
    var reg2 = /^\d$/;//->只能是一个0-9之间的数字
    console.log(reg2.test("9"));//->true
    console.log(reg2.test("012"));//->false

    正则是由元字符组成的:

    每一个正则表达式都是由元字符和修饰符组成的

    什么是[元字符]:在//之间具有意义的一些字符

    • 1、具有特殊意义的元字符
    - \:转义字符,转译后面字符所代表的含义
    - ^:以某一个元字符开始
    - $:以某一个元字符结尾
    - \n:匹配一个换行符
    - .:除了\n以外的任意字符
    		var reg = /^0.2$/;//->以0开头,以2结尾,中间可以是除了\n的任意字符
    		console.log(reg.test("0.2"));//->true
    		console.log(reg.test("0和2"));//->true
    		console.log(reg.test("0-2"));//->true
    		console.log(reg.test("0sdhfohsdf-2"));//->false
    		
    		reg = /^0\.2$/;//->以0开头,以2结尾,中间是.的字符串;
    		console.log(reg.test("0.2"));//->true
    		console.log(reg.test("0-2"));//->false
    - ():功能一:分组 ->把一个大正则本身划分成几个小的正则
    		var reg1 = /^\d+zhu\d+$/;
    		var reg2 = /^(\d+)zhu(\d+)$/;
    		
    		//匹配
    		console.log(reg1.test("2zhu3"));//true
    		console.log(reg2.test("2zhu4"));//true
    		//捕获
    		console.log(reg1.exec("2zhu3"));//["2zhu3", index: 0, input: "2zhu3"]
    		console.log(reg2.exec("2zhu4"));//["2zhu4", "2", "4", index: 0, input: "2zhu4"]
    	- 功能二:改变|的优先级;功能:替换,"或"操作字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。
    
    - x|y:x或者y中的一个
    - ^x|yz$:**x开头、yz结尾、x开头并且yz结尾、三种都可以**
    - [xyz]:x或者y或者z中的一个
    - [^xyz]:除了三个以外的任何一个字符
    - [a-z]:a-z之间的任何一个字符
    - [^a-z]:除了a-z之间的任何一个字符
    - \d:一个0-9之间的数字   \D:除了0-9之间的数字以外的任何字符
    - \b:匹配一个边界符
    - \w:数字、字母、下划线中的任意一个字符  [0-9a-zA-Z_]
    - \s:匹配一个空白字符 空格、一个制表符、换页符...
    • 2、代表出现次数的量词元字符
    - *:出现零到多次
    - +:出现一到多次
    - ?:出现零次或者一次
    - {n}:出现n次
    - {n,}:出现n到多次
    - {n,m}:出现n到m次
    		var reg = /^\d+$/;
    		console.log(reg.test("2015"));//->true

    一个简单的验证手机号的正则:11位数字,第一位是1

    var reg = /^1[3|4|5|7|8]\d{9}$/;//第一位以1开始,第二位可以是34578中的任意一位,后面再加9个数字;
    console.log(reg.test("12345678901"));//false
    console.log(reg.test("13856487589"));//true
    console.log(reg.test("12114567895"));//false

    本文由 GitVP 从 GitHub 收录并在站内全文呈现,版权归原作者所有。

    ← 回到全部文章

    同分类还有