文章目录
ECMAScript5 知识点总结
一、ES5中的严格模式(了解一下)
1、 严格模式:为JavaScript定义了一种不同的解析与执行模型,简单来说他就是一个代码规范(约束),如果在这种模式下没有按照其约定的规范来开发,那么解析器就会抛出错误!
2、 目的:
- 消除Javascript语法的一些不合理、不严谨之处,减少怪异行为;
- 消除代码运行的某些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
- 约束开发者,协助程序员养成良好的编码习惯
3、使用方法:
- 严格模式应用在两种作用域,一种是整个脚本作用域,一种是单个函数作用域
- 整个脚本作用域:需要在js脚本文件顶部使用
'use strict';
(整个js文件都要遵循严格模式来编写) - 单个函数作用域:例如在某个函数内部使用
'use strict';
(函数内部的代码要遵循严格模式) 这种情况下,严格模式只对函数内代码起作用 (在函数内部启用严格模式相对安全)
function a() {
'use strict';
console.log("I'm a web developer")
function b() {
console.log("I'm a web developer too")
}
}
为避免文件合并时发生冲突,尽量把脚本文件放到立即执行函数内部,就没有问题了
;(function a() {
'use strict';
console.log("I'm a web developer")
function b() {
console.log("I'm also in strict mode");
}
})()
这里在开头加分号的目的是为了避免在文件合并时出现下列问题:
var one = function(oneNumber) {
return function(number) {
console.log(oneNumber+ number);
}
}(function(name) { // 此处代码被当成参数传给one函数了
console.log('hello ' + name);
})('Scott');
4、具体有什么:
- 变量必须使用var声明,杜绝不小心将本地变量声明成一个全局变量造成其他变量被污染。
- 对超出权限的操作显示报错,不再做静默失败处理。
- 禁止删除变量和对象中不可删除的属性,显示报错。
- 禁止对象属性重名
- 禁止函数参数重名
- 禁止使用八进制数字
- 禁止使用with语句
- 强制为eval创建新作用域
- 禁止对eval和arguments做非法操作
- arguments不再追踪参数变化
- 禁止使用arguments.callee
- 禁止this指向全局
- 禁止使用function直接引用caller和arguments
- 函数必须声明在整个脚本或函数层面
- 新增一些保留字,不能使用他们作为变量名或参数名(implements, interface, let, package, private, protected, public, static, yield)
二、两种类型(基本类型、引用类型)⭐
个人理解:JS语言中有两种数据类型,分别是“基本数据类型”和“引用数据类型”,他们两个的显著区别就是两者的存储方式、访问方式不同,基本数据类型有五种,分别是number、string、boolean、undefined、null
;引用数据类型有Object、Array、Function
三种。其实后两种都属于Object
1、基本类型
- Number类型:JS语言中所有数值统一都是number类型,不分整型和浮点型,默认基数是10,当然也可以存储八进制、十六进制,但在进行算术计算时,所有以八进制和十六进制表示得数值最终都将被转换成十进制数值。
- String类型:String类型用于表示由零或多个16为Unicode字符组成的字符序列,也就是字符串 。可以通过toString()方法来将其他类型转换为字符串
- Boolean类型: boolean表现的是真和假,true或者false,对于其他类型的转换是任何非空字符串可以转化为true;任何非0和NaN的数字转化为true;任何对象都是true;值得一说的是null转化为false,而undefined不等价于true也不等价于false,但是!undefined却等价于true。
- Undefined类型:未定义类型,他只有一个值那就是它本身undefined,在使用var声明变量但未对变量初始化时,这个变量的值就是undefined
- Null类型:也是只有一个值,就是他的本身(Null),null是一个空对象指针,这也正是用typeof检测null的时候会返回Object的原因,所以我们在声明一个变量若这个变量将来要指向一个对象的的话,我们就可以提前给这个变量赋值为null
1.1.1 数值范围:由于内存限制ES能够保存得最小数值保存在Number.MIN_VALUE中,能够保存的最大数值保存在Number.MAX_VALUE,如果某次计算值超过最大或者最小值,则返回Infinity(正无穷)或-Infinity(负无穷)值。
1.1.2 NaN(Not a Number):是一个特殊的数值,用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。NaN很特别,任何设计到它的值都要变成它本身(NaN),并且NaN ≠ NaN,它和任何值都不相等包括他本身。
2、引用类型
Object类型:object,可以说是一组属性与方法的集合,它可以是数组,还可以是function,关于属性和方法,可以扯到原形链,暂时先不说,单说对象;对象是对一个内存地址的占用,凡是以对象赋值的变量都是对对象的地址的引用,只有以同一个对象赋值的两个变量才相等,否则它就是两个不同内存地址的引用,根本不存在可比性。至于对象中的值的引用,不同的对象有不同的方法,下标或者其他。
两者之间的区别:
- 对于存放两种数据类型的变量: 存放 js基本数据类型的变量存放的是基本类型数据的实际值,而存放引用数据类型的变量是保存对它的引用即指针
- 变量的交换(复制变量时): 对于存放基本数据类型的变量的交换,等于在一个新的作用域创建一个新的空间,新空间与原有的空间不会相互影响;对于存放引用数据类型的变量的交换,并不会创建一个新的空间,而是让对象或方法和之前对象或方法同时指向一个原有空间(即一个地址)
- 声明变量时不同的内存分配: 基本数据类型值(原始值)是存储在栈(stack)中的数据段,即直接存储在变量访问的位置;引用数据类型值是存储在堆(heap)中的对象即存储在变量处的值是一个指针,指向存储对象的内存地址(在栈内存中)。
- 参数传递的不同 : 首先要明确ECMAScript中所有的函数的参数都是按值来传递的;变量存储的基本类型的值只是把值传递给参数之后参数和这个变量互不影响;变量存储的引用数据类型值存储的是该引用值在堆内存中的内存地址,因此传递的值就是这个内存地址。
总结:基本类型值与引用类型值具有以下特点
- 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中
- 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本
- 引用类型的值是对象,保存在堆内存中
- 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针
- 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此最终两个变量最终都指向同一个对象
- 确定一个值是哪种基本类型可以使用typeof操作符,而确定一个值是哪种引用类型可以使用instanceof操作符
三、执行环境及作用域(个人认为很重要)⭐⭐
1、执行环境概念
执行环境:是JavaScript中最重要的概念,执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。我们无法通过代码去访问这个对象,但是他确实存在,并且解析器在处理数据时在后台使用它。
全局执行环境:根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。例如在浏览器里面,那么全局执行环境就是window,因此所有的变量和函数都是window的属性和方法创建的;
来个例子:
var color = 'blue';
function changeColor() {
var anotherColor = 'red'
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问tempColor、anotherColor、color
}
swapColors() // 这里可以访问anotherColor、color但是不能访问 tempColor
}
changeColor() // 这里只能访问 color
这个例子中一共有三个执行环境,全局环境、changeColor()环境、swapColors()环境;全局环境中有一个color变量和一个changeColor()函数,changercolor()的局部环境中有一个anotherColor变量和swapColors()函数,而swapColors()局部环境中有一个变量tempColor变量,无论是全局环境还是changeColor()环境都无权访问tempColor,而作用域链就是这么来的,任何一个环境它可以向上逐步搜索要找的属性,但是它不能向下搜索,这也正是为什么我们在全局环境和局部环境changeColor()中访问不到tempColor属性,反之则可行的原因(由外到内,全不在;由内到外,全都来)
2、作用域概念
作用域:一段程序代码中所用到的名字并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
举个例子:
function outer(){
// 声明变量
var name = "hanfei";
// 定义内部函数
function inner() {
console.log(name); // 可以访问到 name 变量
}
}
console.log(name); // 报错,undefined
说白了作用域就是某个变量他能起作用的区域
作用域链:我们知道在创建一个函数的时候,此函数就会有一个执行环境,在此执行环境内部创建一个变量,内部可用,但对外部是不可见的(如上例),假设我们在inner()函数内部又创建一个函数other(),同事在other()内部声明一个函数age,那么我们在访问inner()函数内部的变量包括函数outer()内部的变量时,都可以访问到,反过来outer()函数想要访问inner()和other()两个函数内部的变量时,就undefined了,如下例(仅仅是一个例子):
function outer(){
// 声明变量
var name = "hanfei";
// 定义内部函数inner
function inner() {
// 生命变量
var text = 'helloworld'
console.log(name); // 可以访问到 name 变量
// 定义内部函数 other
function other() {
var age = 18;
conosle.log(name) // 可以访问到 name 变量
console.log(text) // 可以访问到 text 变量
}
console.log(age) // 报错 undefined
}
console.log(text) // 报错 undefined
}
console.log(name); // 报错,undefined
也就是说,向下访问不可行,向上访问可行。当我们在函数other内部访问一个他自身内部没有的属性时,就会向他上一级(在此就是inner函数内部去查找是否有无他所需的属性,如果没有那就继续更上一层楼到outer内部找是否又他需要的属性,同理如果还没有就继续找,一直找到全局执行环境内,如果没就报错,如果有就使用,那么向上级找自身所需要的变量的过程,就是作用域链)
延长作用域链:就是调用某个对象中的属性,一般用with来实现,将with中的参数设置为某个对象,with的出现会将所传参数中的对象推到当前执行环境作用域的最前端,使当前代码无需向上一级一级查找,但是with有弊端,很少使用。
四、预解析 ⭐
代码执行过程简述:JS代码的执行是由浏览器中的js解析器来执行的,js解析器执行js代码的时候,分为两个过程分为预解析过程和代码执行过程。
预解析:代码在执行前,它内部函数以及变量被提升到当前作用域的最前面,我们称这个过程为预解析。(直接看代码)
console.log(one) // 输出 undefined
var one = 'helloworld'
console.log(one) // 正常输出
function two() {
console.log(twoProperty)
var twoProperty = 123
}
two() // twoProperty undefined
我们在创建one、towProperty这两个变量之前输出了这两个变量,他们的结果并没有报错,只是输出了undefined,这个时候js解析器相当于帮我们提前声明了这两个变量,如下例所示:
var one
console.log(one) // 输出 undefined
one = 'helloworld'
console.log(one) // 正常输出
function two() {
var twoProperty
console.log(twoProperty)
twoProperty = 123
}
two() // undefined
变量的提升只会在当前的作用域中提升,提前到当前作用域的最上面。函数中的变量只会提升到函数作
用域中的的最前面。
函数表达式预解析提升问题:所谓函数表达式,就是我们用变量赋值的形式创建函数,那么这个时候,我们在函数创建之前来调用函数的话,就会报错,如下例:
two() // 报错:two is not a function
var two = function () {
var twoProperty
// console.log(twoProperty)
twoProperty = 123
}
此处函数表达式的写法其实是赋值,并不是函数声明,因此解析器只会提升var two 而var two = function () {…}变量赋值不会提升,所以报错!
五、apply() 和 call() 方法 ⭐
前言: 本着研究的态度去写文章,应当写出自己思考后的结论,但以我现在对此知识点的理解程度还不是很彻底,虽说会简单应用,但是在某种情况下的this方面还是有些模糊并且说服不了自己,此结论先引用书本上比较经典的句子吧,之后再做详细总结。
概述: 每个函数都包含两个非继承而来得方法:apply() 和 call()。这两个方法的用途都是在特定作用域中调用函数,实际上等于设置函数体内this对象的值。
两者之间的区别:apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以是argunments对象。上例子:
function sum (num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 传入arguments对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 传入数组
}
alert(callSum1(10, 10)); // 20
alert(callSum2(10, 10)); // 20
在此。callSum1()在执行sum()函数时传入了this(因为是在全局作用域中调用的,所以传入的就是window对象)和arguments对象,而callSum2同样也调用了sum()函数,但它传入的则是this和一个参数数组。两种情况都可以正常返回。
再来看call方法:
function sum (num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2)
}
alert(callSum(10, 10)); // 20
从上述两个例子来看,call方法与apply方法的唯一区别就是在传参数上的形式不同,apply是一数组形式传递第二个参数,而call方法是将参数逐个列举出来。
敲重点:this总是指向调用某个方法的对象,但是使用call()方法和apply()方法时,就会改变this指向
六、基本包装类型、Function 类型(万物皆对象)
前言:JS是一门弱类型语言,不像Java、C#,规定很松散,乍一看给人一种不严谨的感觉,但是JS有自己的一套方式。当碰到疑惑时,一句万物皆对象似乎就能说服自己。(完全是个人看法)我们在创建一个字符串变量之后,我们可以用这个字符串变量调用方法来对变量进行操作。按照常理来说,一个基本类型的变量是不能调用方法的,只有对象才可以。但在js里面这样做就没问题!下面就来介绍一下
基本包装类型:为了便于操作基本类型值,ECMAScirpt还提供了 3 个特殊的引用类型:Boolean、Number、String。这些类型与其他引用类型相似,但同时也具有与各自的基本类型相应的特殊行为。实际上每当读取一个基本类型的时候,后台就会创建一个对应的基本包装类型的对象,从而能够让我们调用一些方法来操作这些数据。比如:
var one = "some text"
var two = one.substring(2);
其实为了让我们实现这种直观的操作,后台已经自动完成了一系列的处理。那后台到底做了什么呢?
换成代码的形式如下:
var one = new String("some text"); // 创建String类型的一个实例;
var two = one.substring(2); // 在实例上调用指定的方法;
one = null; // 销毁这个实例;
经过后台处理,字符串也就变得跟对象一样了,这种方式同样使用在Boolean和Number类型对应得布尔值和数字值。
引用类型与基本包装类型的区别:主要区别就是对象的生存期,使用new操作符创建的引用类型的实例在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁,因此我们没办法在运行时为基本类型值添加属性和方法。
要注意的是,使用 new 调用基本包装类型的构造函数,与直接调用同名的转型函数是不一样的。例如:
var value = "25";
var number = Number(value); //转型函数
alert(typeof number); //"number"
var obj = new Number(value); //构造函数
alert(typeof obj); //"object"
1、Boolean类型
Boolean 类型是与布尔值对应的引用类型。要创建 Boolean 对象,可以像下面这样调用 Boolean构造函数并传入 true 或 false 值
var booleanObject = new Boolean(true);
Boolean 类型的实例重写了valueOf()方法,返回基本类型值true 或false;重写了toString()方法,返回字符串"true"和"false"。可是,Boolean 对象在 ECMAScript 中的用处不大,因为它经常会造成人们的误解。其中最常见的问题就是在布尔表达式中使用 Boolean 对象,如下:
var falseObject = new Boolean(false);
var result = falseObject && true;
alert(result); //true
var falseValue = false;
result = falseValue && true;
alert(result); //false
在这个例子中,我们使用 false 值创建了一个 Boolean 对象。然后,将这个对象与基本类型值 true构成了逻辑与表达式。在布尔运算中,false && true 等于 false。可是,示例中的这行代码是对falseObject 而不是对它的值(false)进行求值。布尔表达式中的所有对象都会被转换为 true,因此 falseObject 对象在布尔表达式中代表的是 true。结果,true && true 当然就等于 true 了
基本类型与引用类型的布尔值还有两个区别。首先,typeof 操作符对基本类型回"boolean",而对引用类型返回"object"。其次,由于 Boolean 对象是 Boolean 类型的实例,所以使用 instanceof操作符测试 Boolean 对象会返回 true,而测试基本类型的布尔值则返回 false。
alert(typeof falseObject); //object
alert(typeof falseValue); //boolean
alert(falseObject instanceof Boolean); //true
alert(falseValue instanceof Boolean); //false
2、Number类型
Number 是与数字值对应的引用类型。要创建 Number 对象,可以在调用 Number 构造函数时向其中传递相应的数值。下面是一个例子:
var numberObject = new Number(10);
与 Boolean 类型一样,Number 类型也重写了 valueOf()、toLocaleString()和 toString()方法。重写后的 valueOf()方法返回对象表示的基本类型的数值,另外两个方法则返回字符串形式的数值。
简单写几个方法:
- toString()方法:传递一个表示基数的参数,告诉它返回几进制数值的字符串形式;
- toFixed()方法:会按照指定的小数位返回数值的字符串表示;
- toExponential()方法:该方法返回以指数表示法(也称 e 表示法)表示的数值的字符串形式;
敲黑板:不建议直接实例化 Number 类型,而原因与显式创建 Boolean 对象一样。具体来讲,就是在使用typeof 和 instanceof 操作符测试 基本类型数值 与 引用类型数值 时,得到的结果完全不同
3、String类型:
String 类型是字符串的对象包装类型,可以像下面这样使用 String 构造函数来创建:
var stringObject = new String("hello world");
String 对象的方法也可以在所有基本的字符串值中访问到。其中,继承valueOf()、toLocaleString()和 toString()方法,都返回对象所表示的基本字符串值
String 类型的每个实例都有一个 length 属性,表示字符串中包含多个字符
var stringValue = "hello world";
alert(stringValue.length); //"11"
字符方法:
用于访问字符串中特定字符的两个方法分别是:charAt()和 charCodeAt()。这两个方法都接收一个参数,即基于 0 的字符位置。其中,charAt()方法以单字符字符串的形式返回给定位置的那个字符(ECMAScript 中没有字符类型)。一个好例子:
var stringValue = "hello world";
alert(stringValue.charAt(1)); //"e"
字符串"hello world"位置 1 处的字符是"e",因此调用 charAt(1)就返回了"e"。如果你想得到的不是字符而是字符编码,那么就要像下面这样 使用 charCodeAt()了。
var stringValue = "hello world";
alert(stringValue.charCodeAt(1)); //输出"101"
这个例子输出的是"101",也就是小写字母"e"的字符编码。
ECMAScript 5 还定义了另一个访问个别字符的方法。在支持此方法的浏览器中,可以使用方括号加数字索引来访问字符串中的特定字符
var stringValue = "hello world";
alert(stringValue[1]); //"e"
列举单个方法:
- trim()方法:法会创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果;
var stringValue = " hello world ";
var trimmedStringValue = stringValue.trim();
alert(stringValue); //" hello world "
alert(trimmedStringValue); //"hello world"
- 位置方法(从字符串中查找子字符串的方法):indexOf()和 lastIndexOf();这两个方法都是从一个字符串中搜索给定的子字符串,然后返子字符串的位置(如果没有找到该子字符串,则返回-1)。这两个方法的区别在于:indexOf()方法从字符串的开头向后搜索子字符串,而 lastIndexOf()方法是从字符串的末尾向前搜索子字符串;
var stringValue = "hello world";
alert(stringValue.indexOf("o")); //4
alert(stringValue.lastIndexOf("o")); //7
indexOf():传一个参数的时候(要查找字符或字符串),那就是当前字符串从左往右开始找,直到找到与所传参数匹配的字符串为止,返回当前被匹配到的字符串的索引值;如果传两个参数时,那么第二个参数(数值也即索引值)则是控制从当前字符串中哪个索引值开始往右边查,查到就返回。
lastIndexOf():传入一个参数时(要查找字符或字符串),意思就是从当前字符串最右边开始往左边查找,匹配到就返回当前被匹配到的字符的索引;如果传入两个参数时,那么第二个参数(数值也即索引值)则是控制从当前字符串中找到给定的索引值开始往左查,查到返回
var stringValue = "hello world";
alert(stringValue.indexOf("o", 6)); //7
alert(stringValue.lastIndexOf("o", 6)); //4
- 基于子字符串创建新字符串的方法:slice()、substr()、 substring()
这三个方法都会返回被操作字符串的一个子字符串,而且也都接受一或两个参数。第一个参数指定子字符串的开始位置,第二个参数(在指定的情况下)表示子字符串到哪里结束。具体来说,slice()和substring()的第二个参数指定的是子字符串最后一个字符后面的位置。而 substr()的第二个参数指定的则是返回的字符个数。如果没有给这些方法传递第二个参数,则将字符串的长度作为结束位置。与concat()方法一样,slice()、substr()和 substring()也不会修改字符串本身的值——它们只是返回一个基本类型的字符串值,对原始字符串没有任何影响(不用死记,只要遇到问题能想到有这个方法可以解决就行!!!)
var stringValue = "hello world";
alert(stringValue.slice(3)); //"lo world"
alert(stringValue.substring(3)); //"lo world"
alert(stringValue.substr(3)); //"lo world"
alert(stringValue.slice(3, 7)); //"lo w"
alert(stringValue.substring(3,7)); //"lo w"
alert(stringValue.substr(3, 7)); //"lo worl"
Function 类型:
- 概述:函数实际上是对象,每个函数都是Function类型的实例而且都与其他引用类型一样具有属性和方法。
由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。 - 没有重载
将函数名想象为指针,看下例子:
function add(num){
return num+100;
}
function add(num){
return num+200;
}
var result=add(100);//300;
显然,这个例子中声明了两个同名函数,而结果是后面的函数覆盖了前面的函数。
- 函数声明与函数表达式
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁;解析器会率先读取函数声明,并使其在执行任何代码之前可用;至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
//函数声明:三种不同的方式
function sum(num1.num2) { // 函数声明方式
return num1+num2;
}
var sum=function(num1,num2) { // 函数赋值方式
return num1+num2;
}
var sum =new function("num1","num2","return num1+num2"); // 实例化对象方式
-
函数内部属性
在函数内部,有两个特殊的对象:arguments 和 this
arguments是一个类数组对象,包含着传入函数中的所有参数。它有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。它将函数名与函数的执行解耦。this 与JAVA C#中的类似。this引用的函数执行的环境对象,this在代码执行过程中引用不同的对象。
-
函数属性和方法(上一节已经详细介绍过)
· 每个函数都包含两个属性:length和prototype,其中length表示函数希望接受的命名参数的个数。
· 每个函数都包含两个非继承而来的方法:apply()和call()
· 这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。
· 首先,apply()方法接受两个参数:一个是在其中运行函数的作用域
· 另一个是参数数组。可以是Array实例,也可以是arguments对象。
· call方法与apply方法的作用相同,他们的区别在于接受参数的方式不同。
· 使用call时,传递的参数必须逐个列举出来。
七、JavaScript面向对象程序设计(构造函数、对象、原型对象) ⭐
官方定义对象:无序属性的集合,其属性可以包含基本值、对象或者函数。
在总结面向对象之前,我觉得有一个东西很重要,也就是书中所提到的属性类型。再此简单提一下
属性类型:ECMAScript5.1在定义只有内部才用的特性时,描述了属性的各种特征。ECMAScript5.1定义这些特性是为了实现JS引擎用的,因为JS中不能直接访问他们。为了表示特性的内部值,该规范把他们放到了两对儿方括号中,例如[[Enumerable]]
- [ [ Configurable ] ]:表示能否通过delete删除属性从而重新定义新属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值:true,如果为false,表示不能修改属性
- [ [ Enumerable ] ]:表示能否通过for-in循环返回属性,默认值:true
- [ [ Writable ] ]:表示能否修改属性,默认值:true
- [ [ Value ] ]:包含这个属性的数据值,默认值:undefined
要修改某个属性的特性,必须使用ES5的Object.defineProperty()方法。这个方法接受三个参数:属性所在对象、属性的名字、一个描述符对象 ,其中描述符对象(descriptor)对象的属性必须是Configurable、Enumerable 、Writable 、Value 。设置其中的一个或多个值,可以修改对应的特性值。上例子:
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "HANFEI"
});
alert(person.name) // HANFEI
person.name = "han";
alert(person.name) // HANFEI
上述调用defineProperty方法改变person的name属性,让啊不可写只可读,然后我们发现如果尝试修改他的值,将会被忽略,如果在严格模式下会报错。
总结:在调用 Object.defineProperty() 方法创建一个新的属性时,如果不指定,configurable、enumerable、writable特性的默认值都是false。如果调用Object.defineProperty()方法只是修改已定义的属性的特性,则无此限制。多数情况下,可能没必要利用 Object.defineProperty()方法提供的这些高级功能。不过,理解这些概念对理解JavaScript对象却非常有用。
访问器属性:
访问其属性不包含数值,它们包含一对儿getter和setter函数(这两个函数多不是必须的)
在读取访问器的属性的时候,会调用getter函数,这个函数负责返回有效的值;
在写入访问器属性时,会调用setter函数并传入新值
- [ [ configurable ] ]:表示能否通过deleted删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值:true,如果为false,表示不能修改属性
- [ [ enumerable ] ]:表示能否通过for-in循环返回属性,默认值:true
- [ [ get ] ]:在读取属性时调用函数,默认值undefined
- [ [ set ] ]:在写入属性时调用函数,默认值undefined
创建对象
创建对象的方式有很多种,要从最开始的方式一步一步走,每出现一个新方式,都会有它出现的原因(肯定时解决了某个问题),接下来就循序渐进,挨个解释创建对象方式。
- 工厂模式创建对象:工厂模式其实就是用函数来封装特定接口创建对象的细节。
function createPerson(name, age, job) {
var preson = new Object();
person.name = name;
person.age = age;
person.job = job;
person.sayName = function() {
alert(this.name);
};
return person
}
var personOne = createPerson("HF", 18, "banzhuan");
var personTwo = createPerson("HFf", 180, "banzhuan");
我们用createPerson()函数,把创建一个对象并且为对象添加属性以及方法封装到这个函数中去,在需要创建一个对象的时候,调用这个createPerson()函数并传入具体参数,就成功创建了一个对象。
解决的问题:可以创建多个相似对像。
新的问题:无法识别对象,也即不知道一个对象的类型。
- 构造函数模式创建对象:
在强类型语言java中,实例化对象都需要类、构造函数,类是一个抽象概念,在JS中并没有提到类,只是在实例化一个对象的时候用的是构造函数,虽然没明确说明,但是构造函数在JS中似乎扮演的就是类这个角色,ECMAScript中的构造函数可用来创建特定类型的对象,比如一些原生构造函数Object、Array,当然在JS里面也可以自己定义构造函数(构造函数什么名字,实例化出来的对象就是什么类型的)从而定义自定义对象的属性和方法。举例:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayHi = function() {
alert(this.name)
}
}
var personOne = new Person("韩飞", 18, "搬砖") // 我们可以称这对象就是Person类型
var personTwo = new Person("阳哥", 3, "砌墙")
此函数的和普通函数稍微有些区别,他的名字首字母是大写的,没有return语句,并且直接将属性和方法给了this对象(首字母大不大写没做严格要求,为了便于区分,定义构造函数时尽量首字母大写)构造函数和其他函数唯一的区别就是调用方式不同,构造函数也是函数,不才在什么定义构造函数的特殊语法,任何函数只要通过new来调用,那它就可以作为构造函数,否则的话和普通函数没什么区别。
在实例化对象的时候,用到了一个关键字—new,那么new起了什么作用?
JS中new做了什么?:
第一步:创建一个新对象;
第二步:将新对象的_proto_指向构造函数的prototype对象;
第三步:将构造函数的作用域赋值给新对象 (也就是this指向新对象);
第四步:执行构造函数中的代码(为这个新对象添加属性);
第五步:返回新的对象;
补充:上例中实例化的两个对象都有一个constructor属性的,这个属性指向Person。
优点:可以知道某个对象是什么类型了,也可通过传入不同参数来实例化同种类型对象
新的问题:在实例化每个方法的时候,同样的方法都会在每个实例中重新创建一遍,比如上述例子的sayHi()方法,我们可以通过这种方式来解决:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayHi = sayHi
}
function sayHi() { // 实例化的对象都能共享全局作用域中的此方法
alert(this.name)
}
但是!又有问题出现:在全局作用域中定义的函数,实际上只能被某个对象调用,这让全局作用域有点名副其实。还有一个问题就是,如果对象需要很多方法,就需要定义很多全局函数,于是我们自定义的引用类型Person就丝毫没有封装性可言了。
- 原型模式:
在构造函数创建对象中,遇到了新问题,无法定义很多个全局方法,那么此时,原型对象的出现就解决了这个棘手的问题。
prototype原型对象:我们都知道,在创建每一个函数的时候,这个函数都会有个prototype属性,它就相当于一个指针,指向某个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,来个例子:
function Person() {}
Person.prototype.name = "韩飞"
Person.prototype.age = 18
Person.prototype.getName = function() {
console.log(this.name)
}
var one = new Person()
console.log(one)
我们在此例中,我们把所有属性和方法都直接添加给了Person构造函数的原型对象中,实例化的每个对象访问的都是同一组属性和同一个方法。
理解原型对象:
每当创建一个函数时,系统就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性。这个属性时prototype属性所在函数的指针,也即Person.prototype.constructor 指向 person。创建了自定义构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承过来的。当我们调用一个构造函数实例化对象后,每个实例内部都有一个指向构造函数的原型对象的指针,他名字时[ [ prototype ] ],但是在脚本中我们无法访问到,但是自己观察我们会发现,在浏览器(Firefox、Safari、Chrome)的正常支持下,都支持一个属性那就是_proto_;而在其他实现中,这个属性对脚本是完全不可见的。这个_proto_指向构造函数的原型对象。要明确一点,这个链接直存在于实例与构造函数的原型对象之间,而不是在于实例与构造函数之间
八、理解原型链和继承之间的关系以及重要作用。⭐⭐
-
继承:在众多面向对象语言中,都支持两种继承方式:接口继承和实现继承,接口继承只继承方法签名,而实现继承则继承实际方法。在此ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
-
原型链:ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor属性)而实例都包含一个指向原型对象的指针_proto_。 那么假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是所谓的原型链的基本概念。
举个例子:
function OneType() {
this.property = true;
}
OneType.prototype.getOneValue = function() {
return this.property
}
function TwoType() {
this.twoProperty = false;
}
// 改变TwoType构造函数的原型对象指向(指向TwoType的实例)
TwoType.prototype = new OneType()
TwoType.prototype.getTwoValue = function() {
return this.twoProperty;
};
var instance = new TwoType();
console.log( instance.getOneValue()) // true
例子中我们创建了两个构造函数,分别是OneType、TwoType,并给OneType函数内部添加一个属性property,给OneType的原型对象上添加一个方法getOneValue。在构造函数TwoType中,只在内部添加一个属性twoProperty。当我们把TwoType的原型对象指向改变成OneType的一个实例后,紧接着又给TwoType函数的新原型对象添加了一个getTwoValue方法,我们用构造函数TwoType来实例化一个instance对象,最后instance对象可以调用OneType中原型对象的方法。
确定原型和实例的关系:可以通过两种方式来确定原型和实例之间的关系,第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true:
alert(instance instanceof Object) // true
alert(instance instanceof OneType) // true
alert(instance instanceof SubType) // true
第二种方式就是使用isPrototypeOf()方法。同样只要原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true:
alert(Object.prototype.isPrototypeOf(instance)) // true
alert(OneType.prototype.isPrototypeOf(instance)) // true
alert(TwoType.prototype.isPrototypeOf(instance)) // true
原型链的问题:原型链虽然很强大,可以用来继承,但也存在问题,其中主要问题来自包含引用类型值的原型。包含引用类型值得原型属性会被所有实例共享;而这也真实为什么在构造函数中而不是在原型对象中定义属性的原因,在通过原型实现继承是,原型实际上会变成另一个类型的实例,例子:
function SuperType() {
this.colors = ["red","blue","green"]
}
function SubType() {
}
// 继承了 SuperType
SubType.prototype = new SuperType();
var instanceOne = new SubType();
instanceOne.colors.push("pink")
alert(instanceOne.colors) // "red","blue","green","pink"
var instanceTwo = new SubType();
alert(instaneTwo.colors) // "red","blue","green","pink"
当我们把SubType的原型对象指向改变后,并用SubType实例化两个对象,在给instanceOne对象中的colors数组中添加新数组元素时,instanceTwo 对象中的colors数组也会受影响。(实践中很少会单独使用原型链)
常用继承方式:
- 借用构造函数
基本思想:在子类型构造函数的内部调用超类型构造函数。通过使用apply()和call()方法也可以在(将来)新创建对象上执行构造函数。
function SuperType () {
this.colors = ["red","blue","green"]
}
function SubType () {
// 继承了SuperType
SuperType.call(this);
}
var instance1 = new SubType()
instance1.colors.push("pink")
alert(instance1.colors) //
var instance2 = new SubType()
alert(instance2.colors)
完美解决原型链的问题
新的问题:如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了,而且在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
- 组合继承
基本思想:将原型链和借用构造函数的技术组合到一块儿,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType (name, age) {
// 继承属性
SuperType.call(this, name)
this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age)
}
// 用SubType实例化一个对象
var instance1 = new SubType("hanfei", 18)
instance1.colors.push("pink")
alert(instance1.colors) // "red","blue","green","pink"
instance1.sayName() // hanfei
instance1.sayAge() // 18
// 用SubType实例化另一个对象
var instance2 = new SubType("带土", 12)
alert(instance2.colors) //"red","blue","green"
instance2.sayName() // 带土
instance2.sayAge() // 12
就很完美就
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。
九、递归介绍、什么是闭包?关于this对象?
- 递归:
递归就是一个函数通过调用自身的情况下构成的,上例子:
递归实现:求n个数字的和 n=5 ------->5+4+3+2+1
//for 循环写法:
var sum=0;
for (var i=0;i<=5;i++){
sum+=i;
}
console.log(sum);
----------------------分割线---------------------------
function getSum(x) {
if (x==1){
return 1
}
return x+getSum(x-1);
};
var sum1=getSum(5);
console.log(sum1);
console.log(getSum(10));
执行过程:
代码执行getSum(5)—>进入函数,此时的x是5,执行的是5+getSum(4),此时代码等待
此时5+getSum(4),代码先不进行计算,先执行getSum(4),进入函数,执行的是4+getSum(3),等待,
先执行的是getSum(3),进入函数,执行3+getSum(2),等待
,先执行getSum(2),进入函数,执行 2+getSum(1);等待,
先执行getSum(1),执行的是x==1的判断,return 1,所以,
此时getSum(1)的结果是1,开始向外走出去2+getSum(1) 此时的结果是:2+1
执行:
getSum(2)---->2+1
3+getSum(2) 此时的结果是3+2+1
4+getSum(3) 此时的结果是4+3+2+1
5+getSum(4) 此时的结果是5+4+3+2+1
- 闭包:指有权访问另一个函数作用域的变量的函数
要理解闭包,首先必须理解JavaScript特殊的变量作用域。变量作用域无非就是两种:全局变量 和 局部变量。 JavaScript语言的特殊之处,就在于函数内部可以直接读取全局变量。
举个简单例子:
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
f2可以直接访问f1中的变量,但是反过来f1就不能访问f2内部变量
由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c = a();
c();
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的两个特点:
1、作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
2、一个闭包就是当一个函数返回时,一个没有释放资源的栈区。
其实上面两点可以合成一点,就是闭包函数返回时,该函数内部变量处于激活状态,函数所在栈区依然保留。
总结闭包:
1、闭包外层是个函数。
2、闭包内部都有函数。
3、闭包会return内部函数。
4、执行闭包后,闭包内部变量会存在,而闭包内部函数的内部变量不会存在。
闭包用途:
1、可以读取函数内部的变量 ,保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
2、让这些变量的值始终保持在内存中。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
弊端:
1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2 、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
- this对象:this对象的执行环境在不同情况下会发什么变化,并且他的指向也会发生变化,在此我们就简单讨论this在不同情况下他所代表的对象。
1、函数调用模式
该模式就是直接在函数的外部,直接调用该函数执行。
function fun1(){
this.age = 18;
console.log(this);
}
fun1();//函数外部
此时调用fun1就相当于window.fun1(),并且函数内部的this指向window
2、方法调用模式
方法调用模式的语法为 对象名.方法名() 。其中 this 指向当前这个对象。
function fn() {
console.log(this.name)
}
var obj = {
name: 'ls',
sayHi: fn
}
obj.sayHi() // ls
3、构造函数调用模式
构造函数调用模式的语法为 new 函数名() 。其中 this 指向新创建的对象。
function fn() {
console.log(this)
}
var obj = new fn() // fn {}