红皮书第七章学习心得

提醒:本文发布于 2996 天前,文章内容可能 因技术时效性过期 或 被重新修改,请谨慎参考。

TOC
  1. 1. 变量
    1. 1.1. 区别
    2. 1.2. 基本类型
    3. 1.3. 引用类型:
      1. 1.3.1. 例子1:
      2. 1.3.2. 例子2:
      3. 1.3.3. 例子3:
    4. 1.4. 检测方式
      1. 1.4.1. 注意事项
  2. 2. 执行环境和作用域
    1. 2.1. 函数声明方式
      1. 2.1.1. 函数声明
      2. 2.1.2. 函数表达式(匿名函数)
      3. 2.1.3. 递归
    2. 2.2. 闭包和匿名函数
    3. 2.3. 闭包与变量
    4. 2.4. 闭包与内存泄露

变量

变量类型 , 当一个值赋给变量时, 解析器必须确定这个值是基本类型还是引用类型

  • 基本类型: 简单的数据段,按值访问: 有Undefined / null / boolean / number / string
  • 引用类型: 多个值构成的对象 , 不能直接访问对象的内存空间. 实际操作的是它的引用(指针)

区别

这两个类型的区别:

  • 属性: 只有引用类型才能添加属性 , 基本类型无法添加.
  • 复制:

    • 基本类型: 复制一个数据副本. 新创建的变量完全是独立的.
    • 引用类型: 只是新建一个引用(指针) , 两个变量依然指向同一个内存空间.
  • 传递参数: 参数都是按值传递,且只能按值传递

基本类型

这个好理解,略

引用类型:

关键: 按值传递(非常重要)

例子1:

function setName(obj){
obj.name='k'; //即使这个对象是按值传递, obj也会按引用来访问同一个对象. 造成了 按引用传递 的假象.
}
var person = new Object();
setName(person);
alert(person.name) //k

例子2:

function setName(obj){
obj.name="k";
obj = new Object();
//如果是按引用传递,那么obj将被重写了, 输出的应该是x; 在这里,即使是在函数内部改变了参数的值,原始引用依然会保持不变.
obj.name="x"; //并且这个新的obj将引用一个局部对象, 这个局部对象在函数执行完之后立即销毁.
}
var person = new Object();
setName(person);
alert(person.name); //k

例子3:

function setName(obj){
// obj.name="k";
obj = new Object(); //证明了这个是局部对象. 函数执行后被销毁.
obj.name="x";
}
var person = new Object();
setName(person);
alert(person.name); //undefined

检测方式

  • typeof (基本类型检测方式)
  • instanceof (引用类型检测方式)

注意事项

仅仅用typeof或者instanceof来检测引用类型都可能是不靠谱的,为什么不靠谱呢?

以下解释引自红皮书第22章:

Safari(直至第 4 版)在对正则表达式应用typeof操作符时会返回”function”,因此很难确定某个值到底是不是函数。
instanceof操作符在存在多个全局作用域(像一个页面包含多个frame)的情况下,也是问题多多。

如:

var isArray = value instanceof Array;

以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。(别忘了, Array 是 window 的属性。)如果 value 是在另个 frame 中定义的数组,那么以上代码就会返回 false。

所以解决办法是什么呢?

Object.prototype.toString.call(value)

用这来检测引用类型才是最稳妥的

执行环境和作用域

全局环境就是window对象所以的全局变量和函数都是作为window对象的属性和方法创建的.(执行幻境中所有的代码执行完之后,环境就会被销毁,环境内的变量和函数定义也会被销毁)每个函数都有自己的执行环境, 函数在执行时, 函数的环境就会被推入一个环境栈中,然后创建变量对象的 作用域链 , 其作用是保证执行环境对变量和函数的有序访问.执行完之后再弹出.

在函数中,作用域链的最前端是arguments对象,然后逐级向外读取变量,直到全局,全局环境的变量对象始终是作用域链的最后一个对象.

函数声明方式

通常使用以下两种方式:

函数声明

function fname(arg0, arg1, arg2 ){
//...
}

函数声明的重要特征是: 函数声明提升, 在执行代码之前会先读取函数声明. 即便把函数声明放在执行代码的后面也如此.

函数表达式(匿名函数)

var fname=function(arg0, arg1, arg2 ){
//...
}

if(condition){
function sayHi(){ //不能再if语句能使用函数声明!
alert(1);
}
} else{
function sayhi(){
alert(2);
}
}

if(condition){
sayHi = function(){ //函数表达式没有提升作用
alert(1);
}
} else{
sayhi = function(){
alert(2);
}
}

匿名函数常常当成值来使用. 但是不是它的唯一作用.

递归

这是常见的递归函数

function recursive(i){
if(i<=1){
return i;
} else {
//常见的思路是这里写函数名,但是万一本身这个函数名需要更改,就得改动两处
//return i*recurive(i-1);
//所以我们用agruments.callee来替代
return i*arguments.callee;
}
}

再看一个例子

function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1); //注意 : 报错是因为这里用了固定的函数名!!!
// return num * arguments.callee(num-1); //应该这样写
}
}
var anotherFactorial = factorial; //函数别名
factorial = null; //去除名字
alert(factorial(4)); //error!
alert(anotherFactorial(4)); //如果不改动factorial(num-1),那就会报错

所以函数名只是一个地址

闭包和匿名函数

闭包, 是有权访问另一个函数作用域中的变量的函数(注意闭包是函数).常见的闭包创建方式, 在一个函数的内部创建另一个函数.

当函数第一次被调用时,会创建一个执行环境以及相应的作用域链并把作用域链赋给一个特殊的内部属性[[scope]],然后使用this arguments和其他参数来初始化活动对象(即变量),但在作用域链中外部函数的活动对象逐级增加.

例如:

function compare(v1, v2){
if(v1 < v2){
return -1;
} else if(v1 > v2){
return 1;
} else{
return 0;
}
}
var result = compare(5 ,10);

compare内会创建this , arguments ,v1 , v2 的活动对象.全局执行环节的变量对象 this result compare处于第二位.

作用域链本质上是一个执行变量对象的指针列表,它只引用但不实际包含变量对象.一般来说,函数执行完之后, 局部活动对象就会被销毁, 内存中只保留全局作用域.

但是闭包不一样.
比如说,下面这个例子,我们一看以为值是My Object,但是并非如此.

var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()) //The Window

如果要达到期待的效果:

var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this; //this = object,缓存这个想要访问的环境即可
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //My Object

闭包与变量

典型的闭包:

 function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}

作用域链这种配置机制出现一个副作用,闭包只能取得包含函数中任何变量的最后一个值. 因为闭包(是一个函数), 保存的是整个对象,而不是某个特殊的对象.

例子1:

function cf(){
var result = new Array();
for(var i = 0; i<10 ; i++){
result[i]=function(){
return i;
}
}
return result;
}
var fns=cf();
for(var i=0 ; i<10;i++){
document.write(fns[i]()+'<br>');//10个10
}

例子2:

function cf(){
var result = new Array();
for(var i = 0; i<10 ; i++){
result[i]=function(num){
return function(){
return num;
}
}//这里没有i
}
return result;
}
var fns=cf();
for(var i=0 ; i<10;i++){
document.write(fns[i]()+'<br>');
}

例子3:

function cf(){
var result = new Array();
for(var i = 0; i<10 ; i++){
result[i]=function(num){
return function(){
return num;
}
}(i)
}
return result;
}
var fns=cf();
for(var i=0 ; i<10;i++){
document.write(fns[i]()+'<br>');
}

This对象
this对象是基于函数执行环境绑定的, 全局环境中, this==window, 函数为某个对象的方法时且被调用时, this为当前对象.
但是匿名函数的指向环境具有全局性 ,通常指向window. 由于闭包的写法不一样, 可能不太明显

var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
}
};
alert(object.getName()); //"My Object"
alert((object.getName)()); //"My Object"
alert(object.getName) //这里会输出函数的所有代码.
alert((object.getName=object.getName)()); // 所以这里实际上就是将匿名函数放在了全局环境中.因此指向window.
//注意以下写法
var x=10;
alert(x=x); //10

闭包与内存泄露

以下代码容易发生无意识内存泄露:

function fn(){
var el = document.getElementById('el');
el.onclick=function(){
this.style.color="red";
}
}

这段代码获取一个DOM元素并为其设置字体颜色,但它已经发生了内存泄露,为什么?因为el的引用放在了匿名函数中.这在函数内部和本地对象(el)创建了一个循环引用.

改进方法:

function fn(){
document.getElementById('el').onclick=function(){
this.style.color="red";
}
}

访客评论