⭐️ 本文首发自 前端修罗场(点击加入),是
一个由 资深开发者 独立运行 的专业技术社区
,我专注Web 技术、Web3、区块链、答疑解惑、面试辅导以及职业发展
。博主创作的 《前端面试复习笔记》(点击订阅),广受好评,已帮助多人提升实力、拿到 offer。现在订阅,私聊我即可获取一次免费的模拟面试机会
,帮你评估知识点的掌握程度,获得更全面的学习指导意见!
一个完整的javascript实现,应该包括三部分:
DOM是针对XML但是经过扩展用于HTML的应用程序编程接口。
DOM把整个页面映射为一个多层节点结构。通过DOM创建的表示文档的树形图,开发者能够获得控制页面内容和结构的主动权。借助DOM提供的API,我们就可以轻松的删除、添加、替换或修改任何节点。
标签属性async
:立即下载脚本,不妨碍页面中其他操作。只对外部脚本文件有效。
<script async src="a.js">script>
<script async src="b.js">script>
b.js可能会在a.js文件之前执行。
因此,确保两者之间互不依赖非常重要。
建议异步脚本不要再加载期间修改DOM。
异步脚本一定会在页面的load事件前执行,但可能会在 DOMContentLoaded事件触发之前或之后执行。
defer:脚本可延迟到文档完全被解析和显示之后再执行。相当于告诉浏览器——立即下载,但延迟执行。只对外部脚本有效。
注释:无论如何包含代码,只要不存在defer和async属性,浏览器都会按照
type:虽然text/javascript和text/ecmascript都已经不被推荐使用,但人们一直以来还是约定俗成地使用text/javascript。
实际上,服务器在传送javascript文件时使用的MIME类型通常是application/x-javascript,但在type中设置这个值却可能导致脚本被忽略。
考虑到约定俗成和最大限度的浏览器兼容性,目前type属性的值还是text/javascript。如果不写type,默认值也是这个。
注意:不要在脚本中任何语句中出现 字符串,否则会被浏览器认为该段脚本的结束。如果要出现,请通过转义字符解决。 如:
<script type="text/javascript">
function sayScript(){
alert("<\/script>");
}
script>
同样,在解析外部javascript文件时,页面的处理也会暂时停止。
如果是在XHTML文档中,也可以省略前面示例代码中结束的标签,直接使用
但是在html文档中,不是使用该方法。
原因:这张语法不符合HTML规范,而且也得不到某些浏览器(尤其是IE)的正确解析。
有一点:按照惯例,外部javascript文件带有js扩展名。这个扩展名不是必需的,因为浏览器不会检查包含javascript的文件的扩展名。但是,服务器通常还是看扩展名决定为响应应用哪种MIME类型。如果不实用.js扩展名,请确保服务器返回正确的MIME类型。
在XHTML中,会出现一些解析错误。但是这些解析能做html中执行。为了避免这样的错误,例如:在XHTML中,小于号( < )会被当做开始一个新标签来解析。那么通常避免这样错误的方法是:
使用CData片段:即使用CData片段来包含javascript代码。CData片段是文档中的一个特殊区域,这个区域中可以包含不需要解析的任意格式的文本内容。
但是,有些浏览器并不支持CData,那么我们就需要将CData标记进行注释即可。
例如:
<script>
//< ! [CDATA[
function compare(a,b) {
//
}
//]]>
< /script>
这种格式在所有现代浏览器中都可以正常使用。
文档模式:分为混杂模式(不推荐使用)和标准模式。
用在不支持javascript的浏览器中显示替代内容。这个元素可以包含能够出现在文档中得任何HTML元素,
内容只有在下列条件下才会显示出来:
(1)浏览器不支持脚本;
(2)浏览器支持脚本,但脚本本禁用。
例如:
本页面需要了浏览器启用javascript
若要在整个脚本中使用严格模式,可再脚本顶部增加:
"use strict";
这是一个编译指示,用于告诉支持的js引擎切换到严格模式。这是为不破坏ES3语法而特意选定的语法。
严格模式下,javascript的执行结果会有很大不同。
undefined\null\Boolean\Number\String\Object
undefined派生自null,但两者的用途完全不同。
isNaN()用于检测
;var num1=25;
var num2=~num1-1;//num2=-26
本质:操作数的负值减1。
!Object //false;
!NaN //true;
!underfined//true
var result1=("55"==55);//true,转换后相等
var result2=("55"===55);//false,不经转换,不同的数据类型不相等;
区别:
全等——在比较前,不对数据进行转换;
相等——在比较前,对数据进行转换;
牢记:
null==underfined;//true,因为它们是类似的值;
null===undefined;//false,因为他们是不用类型的值;
Tip:由于相等于不等操作符存在类型转换问题,而为了保持代码中数据类型的完整性,推荐使用全等和不全等操作符。
for(property propName in window)
例如:
for(var propName in window){
console.log(propName);
}
注释:我们使用for-in循环来显示BOM中window对象的所有属性。每次执行循环都会将window对象中存在的一个属性名赋值为变量propName。这个过程直到对象中所有属性都被枚举一遍为止。
ES中,对象的属性没有顺序,因此for-in循环输出的属性的顺序不可测。
如果表示迭代的对象的变量值为null或underfined,for-in语句会抛出错误。虽然ES5更正了这一点,进行不执行循环体处理,但是为了兼容性,在使用之前,先检测确认该对象的值是否是null或underfined。
var qs=location.search.substring(1);
var hostname=location.hostname;
var url=location.href;
//使用with对上述代码更改:
with(location){
var qs=search.substring(1);
var hostname=hostname;
var url=href;
}
//即,location是同一个对象
注意:
(1)在with语句中,每个变量被认为是一个局部变量,而如果在局部变量环境中找不到该变量的定义,就会查询location对象中是否有同名的属性。如果有同名属性,则以location对象属性的值作为变量的值。
(2)在严格模式下,不允许使用with语句,否则视为语法错误。
(3)使用with会导致性能下降,也给调式带来困难。
switch()
switch语句在比较值时使用的是全等运算符,因此不会出现类型转换问题。
函数-封装语句
- ES中得函数在定义时不必指定是否返回值;
实际上,任何函数在任何时候都可以通过return语句后跟要返回的值来实现返回值。如:return num1+num2;- 函数在执行完return语句后停止并会立即退出;因此,位于Return之后的语句是永远不会被执行的;
- return语句可以不带任何返回值。在这种情况下,函数在停止执行后将返回underfined值。这种用法一般用在需要提前停止函数执行而又不需要返回值的情况下。
推荐的做法:要么让函数始终有返回值,要么都不要有返回值。否则,如果时有时没有,这会给调试代码带来不便。
严格模式下,对函数有一些规则:
- 把函数命名为eval或arguments;
- 不能把参数命名为eval或arguments;
- 不能出现两个命名参数同名的情况 ;
出现上述情况,会导致代码无法执行。
ES中不介意传递进来多少个参数,不在乎传进来的参数是什么类型;
即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数,可以传递一个、三个甚至不传,解析器不会有什么怨言的;原因——ES中参数在内部是用一个数组来表示的,函数接收到的始终是这个数组,而不关心数组中包含哪些参数。
• 实际上,在函数体内可通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数,如arguments[0]表示第一个参数。
ES函数的一个重要特点:命名的参数只提供便利,但不是必须的;
通过arguments的length属性能够获知有多少个参数传递给了函数;
arguments对象可以与命名参数一起使用。
arguments的值永远与对应命名参数的值保持同步,但是它们的内存空间是独立的。
• 如果只传入一个参数,那么为argument[1]设置的值不会反应到命名参数中。因为arguments对象的长度是由传入的参数个数决定的,不是由定义函数时的命名参数的个人决定的;
没有传递值的命名参数将自动被赋予underfined值。
严格模式下,对arguments的值进行重写,会导致错误;
function doAdd(num1,num2) {
arguments[1]=10;//在严格模式下,这样的重写会导致错误;
}
因为ES函数没有签名(接收参数的类型和数量),其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不能做到的。
模仿重载——通过检查传入函数中参数的类型和数量并作出不同的反应;
例如:
function doAdd() {
if(arguments.length==1){
//
}else if(arguments.length==2){
//
}
}
- 基本类型的值是简单的数据段,引用类型的值是由多个值构成的对象;
- 在将一个值赋给变量时,解析器必须确定这个值是基本类型还是引用类型;
- 引用类型的值是保存在内存中得对象。javascript不允许直接访问内存中得位置,不能直接操作对象的内存空间。在操作对象时,实际上我们操作的是对象的引用。
- javascript中字符串不是引用类型;
创建对象:
var person=new Object();
person.name="jack";//添加name 属性
//如果对象不被销毁或者属性不被删除,这个属性会一直存在
与基本类型不同的是,在赋值的时候,引用类型的变量的值实际上是一个指针,这个指针指向存储在堆栈中的一个对象。赋值操作结束后,两个变量实际上将引用同一个对象。改变其中一个变量的值将影响到另一个变量的值。
例子:
function setName(obj){
obj.name="jack";
}
var person=new Object();
setName(person);
alert(person.name);//jack
//对比下面
function setName(obj){
obj.name="jack";
obj=new Object;
obj.name="mick";
}
var person=new Object();
setName(person);
alert(person.name);//"jack"
说明:在第2段代码中,即使在函数内部修改参数的值,但原始的引用仍然保持不变。实际上,当在函数内部重写obj时,这个变量引用的就是一个局部对象。而这个局部对象会在函数执行完毕后立即被销毁。
在数值类型中,使用typeof能够很好检测数值变量的类型,但是对于引用类型的变量,typeof就不行了。
使用instanceof来检测引用类型的变量,如果是引用类型,那么返回true。如果是数值类型,返回false。
例如:
alert(person instanceof Object) //变量person是Object吗
当执行流进入下列任何一个语句时,作用域链就会加长;
例如:
for(var i=0;i<10;i++){
}
console.log(i);//10
window.变量名
访问全局变量;(1)在js中,一个函数可以作为另一个函数接收的一个参数。
我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。
如:
function execute(someFunction ,value){
someFunction(value);
}
execute(function(word){console.log(word)},"Hello");
函数内部另一个特俗对象是this,this引用的是函数据以执行的环境对象。当在网页的全局作用域中调用函数时,this对象引用的就是window.
在ES5中规范化了另一个函数对象的属性:caller,这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。
例如:
function outer(){
inner();
}
function inner(){
alert(inner.caller);
}
outer();
结果:警告框中显示outer()函数的源代码;因为outer()调用了inter(),所以inner.caller就指向outer()
函数的属性和方法:
(1)每个函数都包含两个属性:length和prototype
其中,length属性表示函数希望接收的命名参数的个数;
prototype属性:保存它们所有实例方法的真正所在。例如,toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用极为重要。在ES5中prototype属性不可枚举,因此使用for-in无法实现。
(2)每个函数都包含两个非继承而来的方法:apply()和call()
apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组(这可以是Array的实例,也可以是arguments对象)。
例如:
function callSum1(num1,num2){
return sum.apply(this,arguments);
//或者
return sum.apply(this,[num1,num2]);
}
function sum(num1,num2){
return num1+num2;
}
call()与apply()作用相同,区别仅在于接收参数的方式不同。对于call,第一个参数是this没有变化,变化的是其余参数都直接传递给函数。——在使用call()方法时,传递给函数的参数必须逐个列举出来。
例如:
function callSum(num1,num2){
return sum.call(this,num1,num2);
}```javascript
使用call()还是apply()取决于你采取哪种给函数传递参数的方式最方便;
apply()与call()真正的用武之地是能够扩充函数赖以运行的作用域。
bind():该方法会创建一个函数实例,其this值会被绑定到传给bind()函数的值。
注意:对类型进行判定最佳的方式是通过
Object.prototype.toString.call(数据)
例如:
Object.prototype.toString.call([])=='[Object Array]'
Object.prototype.toString.call("abc")=='[Object String]'
Javascript具有自动垃圾收集机制。在编写javascript程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。
原理 :找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。
垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于编辑无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略:
(1)标记清除
这是各浏览器最常用的方式。可使用任何方式来标记变量。比如,可通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。
(2)引用计数
这是一种不常见的方式。引用计数的含义:跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得另一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
但是,引用计数会遇到一个严重的问题:循环引用——指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。
(3)性能问题
说到垃圾收集器多长时间运行一次,不禁让人联想到IE因此而声名狼藉的性能问题。IE的垃圾收集器是根据内存分配量运行的,具体一点就是256个变量、4096个对象或数组字面量和数组元素或者64KB的字符串。达到上述任何一个临界值,垃圾收集器就会运行。
这种实现方式的问题在于,如果一个脚本中包含那么多变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量。这样一来,垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使IE7重写了其垃圾收集例程。
在IE7中的垃圾收集机制有所改变的是:如果垃圾收集例程回收的内存分配量低于15%,则变量、字面量和(或)数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界值重置回默认值。这看似简单的调整,却极大提升IE在运行包含大量javascript的页面时的性能。
(4)管理内存
javascript中内存管理面临的最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全考虑,目的是防止运行javascript的网页耗尽全部系统内存而导致系统奔溃。内存限制问题不仅会影响给变量分配内存,同时还影响调用栈以及在一个线程中能同时执行的语句数量。
因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用——即“解除引用”,该方法适用于大多数全局变量和全局对象的属性,而局部变量会在它们离开执行环境时自动被解除引用。例如:
function createPerson(name){
var localPerson=new Object();
localPerson.name=name;
return localPerson;
}
var globalPerson=createPerson("Nicholas")
//手工解除globalPerson的引用
globalPerson = null;
注解:由于localPerson在createPerson()函数执行完毕后就离开了其执行环境,因此无需显示地为其解除引用。而全局变量globalPerson则需要我们在不使用它的时候手工为它解除引用。
不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。引用数据类型有时候也被称为对象定义,因为它们描述的是一类对象所具有的属性和方法。
对象是某个特定引用类型的实例。新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。例如:
var person=new Object();
这行代码创建了Object引用类型的一个新实例,然后把该实例保存在了变量person中。使用的构造函数是Object,它只为新对象定义了默认的属性和方法。
创建Object实例的方式有两种。第一种:
使用new 操作符后跟Object构造函数。
var person=new Object();
第二种:使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。例如:
var person={
name:"jack",
age:20
}
注:使用对象字面量语法时,属性名也可以使用字符串。且通过该方法,实际上不会调用Object构造函数。通常也是开发人员青睐的方法。
一般而言,访问对象属性时使用的是点表示法。不过,在javascript中也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。如:
alert(person["name"]);\\方括号语法
alert(person.name);\\点表示法
建议使用点表示法。
与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据。即,可以用数组的第一个位置来保存字符串,用第二位置来保存数值,用第三个位置来保存对象,以此类推。而且,ECMAScript数组的大小是可以动态调整的,即可以随着数据的添加自动增加以容纳新增数据。
创建数组有两种方式:
(1)、使用Array构造函数
var colors=new Array();//new可省略
(2)、数组字面量表示法
数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开
var colors=["red","blue","green"];
注意:
使用数组字面量创建数组优于数组构造函数。
主要原因:[]与new Array() ,前者2个字符,后者11个字符。
此外,由于javascript的高度动态特性,无法阻止修改内置的Array构造函数,
即new Array()创建的不一定是数组。
因此,推荐使用数组字面量。
数组的length属性很有特点——它不是只读的。通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。
var colors=["red","blue","green"];
colors.length=2;
alert(colors[2]);//undefined
注:数组最多可以包含4294967295个项,这几乎已经能够满足任何编程需求了。如果想添加的项数超过这个上限值,就会发生异常 。
上面的结果也表面,javascript中数组是对象。如果访问不存在的对象,会返回undefined。访问不存在的数组索引,也会返回undefined。
从ECMAScript3之后,出现了确定某个对象是不是数组的经典问题。对于一个网页或者全局作用域而言,使用instanceof操作符就能得到满意的结果:
if(value instanceof Array){
//对数组执行某些操作
}
instanceof的问题在于:它假定只有一个全局执行环境。如果网页中包含对个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。
为了解决这个问题,ECMAScript5新增了Array.isArray()方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。
if(Array.isArray(value)){
//对数组执行某些操作
}
所有对象都具有toLocaleString()、toString()和valueOf()方法。其中,调用数组的toString()方法会返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。而valueOf()返回的还是数组。
eg:
var colors=["red","blue","green"];
alert(colors.toString());//red,blue,green
alert(colors.valueOf());//red,blue,green
alert(colors);//red,blue,green
数组继承的toLocaleString()、toString()和valueOf()方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用join()方法,则可以使用不同的分隔符来构建这个字符串。join()方法只接受一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。
eg:
var colors=["red","green","blue"];
alert(colors.join(","));//red,green,blue
alert(colors.join("||"));//red||green||blue
如果不给join()方法传入任何值,或者给它传入undefined,则使用逗号作为分隔符。IE7及更早版本会错误的使用字符串“undefined”作为分隔符。
注:如果数组中的某一项的值是null或者undefined,那么改值在join()、toLocaleString()和valueOf()方法返回的结果中以空字符串表示。
ECMAScript为数组专门提供了push()和pop()方法
,以便实现类似栈的行为。
ECMASCript也提供了shift()和unshift()
方法。
var colors=["red","green","blue"];
var colors2=colors.concat("yellow",["black","brown"]);
console.log(colors);//red,green,blue
console.log(colors2);//red,green,blue,yellow,black,brown
var colors=["red","green","blue","yellow","purple"];
(1)
var colors2=colors.slice(1);
console.log(colors2);//green,blue,yellow,purple
(2)
var colors3=colors.slice(1,4);
console.log(colors3);//green,blue,yellow
删除:指定2个参数---要删除的第一项的位置和要删除的项数;
var colors=["red","green","blue"];
var removed=colors.splice(0,1);
console.log(colors);//green,blue
插入:指定3个参数---起始位置、0(要删除的项数)、要插入的项,要插入的项可多个;
inserts=colors.splice(1,0,"yellow","orange");
console.log(colors);//green,yellow,orange,blue
替换:指定3个参数---起始位置、要删除的项数、要插入的任意数量的项
replaces=colors.splice(1,1,"red","purple");
console.log(colors);//green,red,purple,orange,blue
indexOf()和lastIndexOf()分别从数组开头向后与末尾开始向前查找;
两个方法,都返回要查找的项在数组中的位置,或者在没找到的情况下返回-1.
注意:在比较第一个参数与数组中每一项时,会使用全等操作符,即要求查找的项必须严格相等。
var numbers=[1,2,3,4,5,4,3,2,1];
var everyResult=numbers.every(function(item,index,array)){
return (item>2);
});
alert(everyResult);//false
some():转入的函数对数组中的某一项返回true,就会返回true;
var someResult=numbers.some(function(item,index,array)){
return (item>2);
});
alert(someResult);//true
filter():利用指定的函数确定是否在返回的数组中包含某一项
var numbers=[1,2,3,4,5,4,3,2,1];
var filterResult=numbers.filter(function(item,index,array)){
return (item>2);
});
alert(someResult);//[3,4,5,4,3]
map():返回一个数组,这个数组的每一项都是在原始数组中的对应项上运行传入函数的结构;
var numbers=[1,2,3,4,5,4,3,2,1];
var mapResult=number.map(function(item,index,array)){
return item*2;
});
alert(mapResult);//[2,4,6,8,10,8,6,4,2]
forEach():对数组中的每一项运行传入的函数,没有返回值;
reduce()和reduceRight(),迭代数组的所有项,然后构建一个最终返回的值。分别从数组第一项与最后一项开始,向前与向后遍历。
接收4个参数:前一个值、当前值、项的索引和数组对象,函数返回的任何值都会作为第一个参数自动传给下一项。
var values=[1,2,3,4,5];
var sum=values.reduce(function(prev,cur,index,array){
return prev+cur;
});
alert(sum);//15
ES中最有意思的莫过于函数,因为函数实际上是对象。
每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。
函数是-对象,函数名是-指向函数对象的指针,不会与某个函数绑定。
(1)、3种函数定义方法:
function sum(num1,num2){
return num1+num2;
}
var sum=function(num1,num2){
return num1+num2;
}
var sum=new Function("num1","num2","return num1+num2");
//Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成是函数体
(2)、没有重载
ES中没有函数重载的慨念。如果同时声明了两个同名函数,那么后一个会覆盖前一个。
(3)、函数声明与函数表达式
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。
注意:
解析器会率先读取函数声明,并使其在执行任何代码之前可用于访问;
而函数表达式,必须等到解析器执行到它所在的代码行,才会真正被解析执行。
例如:
alert(sum(10,10));
var sum=function(num1,num2){
return num1+num2;
}
//上述代码,在第一行就开始报错-"unexcepted identifier(意外标识符)错误",
并且不会执行到下一行。
(4)、作为值的函数
函数本身就是变量,所以,函数也可以作为值来使用。即,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
function callSomeFunction(someFunction,someArgument){
return someFunction(someArgument);
}
可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1=object1[propertyName];
var value2=object2[propertyName];
if(value1<value2){
return -1;
}else if(value1>valu2){
return 1;
}else {
return 0;
}
};
}
(4)、函数内部属性
函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组对象,包含着传入函数中的所有参数。虽然,arguments的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性。
callee属性,是一个指针。指向拥有这个arguments对象的函数。
function factorial(num){
if(num<=1){
return 1;
}else {
return num*arguments.callee(num-1)
}
}
var trueFactorial=factorial;
factorial=function(){
return 0;
};
alert(trueFactorial(5));//120
alert(factorial(5));//0
//变量trueFactorial获得了factorial的值,实际上是在另一个位置上保存了一个函数的指针。
函数内部的另一个特殊对象是this。其行为与java和C#中的this大致类似。this引用的是函数剧以执行的环境对象—或者也可以说是this值(当在网页的全局作用域中调用函数时this对象引用的就是window)。
window.color="red";
var o={color:"blue"};
function sayColor(){
alert(this.color);
}
sayColor();//"red"
o.sayColor=sayColor();
o.sayColor();//"blue"
//sayColor()是在全局作用域中定义的,它引用this对象。由于在调用函数之前,this的值并不确定,因此this可能会在代码执行过程中引用不同的对象。当在全局作用域中调用sayColor()时,this引用的就是全局对象window。即,this.color会转换为window.color求值。而把这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o,因此this.color会转换成对o.color求值。
函数名仅仅是一个包含指针的变量。即使是在不同的环境中执行,全局的sayColor()函数与o.sayColor()指向的仍然是同一个函数。
caller属性:这个属性保存着调用当前函数的函数的引用。如果在全局作用域中调用当前函数,它的值为Null。
function outer(){inner();}
function inner(){alert(inner.caller);}
//inner.caller指向outer(),等价于alert(arguments.callee.caller);
outer();
注意,当函数在严格模式下执行,arguments.callee会导致错误。ES5还定义了arguments.caller属性,但在严格模式下访问它也会错误,在非严格模式下这个属性始终是undefined。
另外,严格模式下,不能为函数的caller属性赋值,否则会导致错误。
(undefined,boolean,number,string,null)
:存放在栈
中,值不可变
,可直接访问。即使是动态修改了基本数据类型的值,它的原始值也是不会改变。
如:
var str = "imaginecode";
console.log(strp[1]="f");
console.log(str); //imaginecode
其次,基本类型的比较是值的比较,只要它们的值相等,就认为相等。
所以,通常为了避免值类型的转换,我们使用严格等 a===b
进行比较。
(object)
存放在堆
中,值可变
。引用类型的变量,实际上是一个存放在栈内存的指针,指向堆内存中的地址。
如:
var a = [1,2,3];
a[1] = 6;
console.log(a); //[6,2,3]
其次,引用类型的比较是引用的比较。所以每次我们对 js 中的引用类型进行操作的时候,都是操作其对象的引用(保存在栈内存中的指针),所以比较两个引用类型,是看其的引用是否指向同一个对象。例如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a===b); //false
虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是说变量 a 和变量 b 指向的不是同一个对象
,所以他们是不相等的。
基于以上,由此可见:
基本类型的赋值是传值,而引用类型的赋值的传地址。
例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。
先看一段代码:
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};
先定义个一个原始的对象 obj1,然后使用赋值得到第二个对象 obj2,然后通过浅拷贝,将 obj1 里面的属性都赋值到 obj3 中。也就是说:
然后我们改变 obj2 的 name 属性和 obj3 的 age 属性,可以看到,改变赋值得到的对象 obj2 同时也会改变原始值 obj1。
而改变浅拷贝得到的的 obj3 则不会改变原始对象 obj1。
这就可以说明赋值得到的对象 obj2 只是将指针改变,其引用的仍然是同一个对象,
而浅拷贝得到的的 obj3 则是重新创建了新对象。
然而,我们接下来来看一下改变引用类型会是什么情况呢,我又改变了赋值得到的对象 obj2 和浅拷贝得到的 obj3 中的 language 属性的第二个值和第三个值(language 是一个数组,也就是引用类型)。结果见输出,可以看出来,无论是修改赋值得到的对象 obj2 和浅拷贝得到的 obj3 都会改变原始数据。
因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。
所以就会出现改变浅拷贝得到的 obj3 中的引用类型时,会使原始数据得到改变。
由此,我们可以知道深拷贝与浅拷贝的一个区别:
深拷贝是对对象以及对象的所有子对象进行拷贝。
思路就是递归调用刚刚的浅拷贝,把所有属于对象的属性类型都遍历赋给另一个对象即可。