liwens / blog Goto Github PK
View Code? Open in Web Editor NEW李文 写博客的 地方
李文 写博客的 地方
作用域是一套查找变量位置和查找范围的规则,他的作用是协助引擎找到需要的变量
如下面代码
var value = 1;
function foo() {
console.log(value)
}
foo() //1
执行foo时,foo的作用域块中,找不到变量value, 这时候根据作用域定义的规则,向上查找,foo的上级的全局作用域,于是在这里找到了value并返回。
同时作用域规定查找变量终点是全局作用域。不管能不能找到,都要给引擎一个交代
作用域会协助引擎查找需要的变量,那么引擎怎样查询的变量呢
引擎有两种查询形式,LHS 和 RHS
L 表示左侧, R表示右侧
引用《你不知道的JavaScript》的话
当变量出现在赋值操作的左侧进行LHS查询,出现在右侧进行RHS查询
LHS查找表示为赋值操作寻找一个容器,而RHS查找只想知道容器里面装了什么
通俗点讲 赋值操作 就是 “=”号
var a = 1;
我们需要对变量a进行赋值操作,显而易见的变量a也在 “=”号左边, 对 a 就是LHS查询,赋值操作之外的查询,就是RHS查询,比如
//对value是LHS查询,对a,b是RHS查询
var value = a + b;
//这里对value是RHS查询
console.log(value)
因为在变量还没有声明的情况下,两种查找返回的结果不一样,
LHS查询从内部作用域向上找直到全局作用域都没找到变量的情况下,引擎会帮你在全局作用域创建一个变量,作为赋值的容器(严格模式下除外,而es6 module模块化自动开启严格模式)
RHS查询在找不到的情况下会,引擎会抛出ReferenceError错误
这就是上面说的`不管能不能找到,都要给引擎一个交代
function foo() {
console.log(a)
}
foo(); //Uncaught ReferenceError: a is not defined
function foo2() {
console.log(a = 1)
}
foo2(); // 1
所以这就是foo2函数在不声明变量a的情况下,也能正常输入 1 的原因了
词法作用域(lexical scoping),也叫静态作用域。 顾名思义静态作用域是相对静止的。 函数的作用域在定义时候就决定了。
我们来看下面代码
var value = 1;
function bar() {
var value = 2;
function foo() {
console.log(value);
}
foo();
}
bar(); //2
我们看到,foo会输出一个2,因为它定义在了bar的内部,所以foo的作用域是 foo => bar => 全局作用域。向上查找的过程中,引擎在bar作用域块发现了变量value,于是返回给了引擎,查找结束。
我们再来看下面一段代码
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
//在bar内部调用
foo();
}
bar(); // 1
我们看到,就算foo是在bar内部调用的,但foo会输出一个1而不是2,原因就是 JavaScript采用词法作用域(静态作用域),函数的作用域在定义时候就决定了。 foo定义在了全局作用域中,他的作用域是 foo => 全局作用域。
在编程语言界中,作用域有两种工作模型, 刚刚介绍的词法作用域(静态作用域) 是最普遍的。 相对的另外一种就是动态作用域了, 如果JavaScript是采用动态作用域的,那么上一个例子输出的就是2,
动态作用域应用的语言比较少,有Bash脚本,Perl。 这里不作过多讲解
参考:你不知道的JavaScript(上卷)
首先,我们看下面代码, 我们提前执行了foo
和bar
.可以看到输出结果不一样, 用函数声明创建的foo能正常输出,用函数表达式创建的bar报错了
foo();// 1
bar();// Uncaught TypeError: bar is not a function
//函数声明 创建
function foo() {
console.log(1)
}
//函数表达式 创建
var bar = function() {
console.log(2)
}
如果有人问为什么时,你肯定能张口就来: “函数声明创建的函数会提升到作用域顶部,可以先使用后定义” 这其实没有什么错误,便于大家理解。但如果要准确的解答这个问题,就要深入到本章节的内容了
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
今天重点讲讲创建变量对象的过程。
变量对象是与 执行上下文 相关的数据作用域,对象里面储存了我们保存的变量和函数声明。
而执行上下文 分为两种
var a = 1;
function foo(i) {
var b = 2;
}
foo(111)
在上述代码中,执行栈中有两个上下文,全局上下文和函数foo上下文,我们用数组的模拟一下
stach = [
globalContext,
fooContext
]
对于全局上下文来说,VO(变量对象)大概是这样的
globalContext.VO = {
a:undefined,
foo: reference to function foo(){},
}
而对于函数 foo
来说,AO(活动对象)是这样的
fooContext.AO {
arguments: {
0:111,
length: 1
},
a: undefined,
i: undefined,
b: undefined,
}
执行上下文的代码会分成两个阶段进行处理,分析和执行,我们也可以叫做
1.进入执行上下文
2.代码执行
当进入执行上下文时,这时候还没有执行代码,JS引擎会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
举个例子:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
在第一阶段进入执行上下文后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
在第二阶段 代码执行 ,这时候的AO是
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
这就是一开始例子说的函数表达式会报错,而函数声明正常执行的原因了。
另外如果变量名称跟已经声明的形式参数或函数相同,则函数声明的权重高。
请看下面代码,
console.log(foo);//会打印函数
function foo(){
console.log("foo");
}
var foo = 1;
console.log(foo);//1
可以看到,提前使用的时候,函数声明的权重高,所以第一个console打印了函数。但这样不会觉得很乱吗?
在我们的潜在意识中,代码从上到下按顺序执行更直观,也不容易出错的。变量提升反而会把人绕晕。所以谷歌建议的代码规范中也倡导使用函数表达式创建函数,而在ES6中,使用let,const等定义变量或者函数,也不会提升, 会存在 暂时性死区
函数的表现
函数提升
bar(); // 2
function bar() {
console.log(2)
}
函数表达式
foo(); // 不管是var还是let定义,都会报错
var foo = function() {
console.log(1)
};
变量的表现
//es5, 变量提升
console.log(foo)//undefined
var foo = 1;
//es6 暂时性死区
console.log(bar)//Uncaught ReferenceError: bar is not defined
let bar = 2;
所以es6的暂时性死区其实就是提前调用变量或函数,在进入上下文时,不会在AO中提前开辟内存。而是直接抛出ReferenceError错误
遵循先定义再使用原则能一定程度降低项目出错的可能,增加项目可维护性
arguments
如果你有留意上面的示例代码,会发现有一个叫arguments的对象。引用《JavaScript权威指南》
调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素。
原型链有几个概念
构造函数,原型,prototype,实例。
首先来说一下构造函数
person 是一个普通的函数
function person() {}
当一个普通函数被new
操作符执行的时候,那么他就是构造函数
function Person() {}
var person = new Person();
person.name = 'liwens';
console.log(person.name); // liwens
另外要注意的是,函数名Person
是以大写P
字母开头的。在《JavaScript 高级程序设计第三版》P145 页中提到
按照惯例,构造函数始终都应该是以一个大写字母开头,而非构造函数应该以一个小写字母开头
原型 是什么? 答: 原型 其实是一个对象,一般以 Xxx.prototype 表示
prototype 是什么? 答: 通俗的讲 prototype 是 构造函数 和 原型 之间的链接 (专业点讲叫 委托)。
每一个对象( 函数也是对象 )在创建的时候都会通过prototype
关联一个对象( null 除外 ),这个对象就是原型。
让我们用一张图表示构造函数和实例原型之间的关系:
实例就是通过 new
操作符调用 构造函数 生成的对象,如下面代码的 person1
就是 实例
function Person() {}
var person1 = new Person();
每一个实例都会自带一个属性 __proto__,这个属性会指向实例的原型
为了证明这一点,我们可以在火狐或者谷歌中输入
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
于是我们更新下关系图
同一个构造函数生成的实例,都指向同一个原型
function Person() {
}
Person.prototype = {
//这里需要注意的是,我们说过原型的一个对象。我们这种写法实际上是把原型指向了另外一个对象。所以需要把constructor属性指回构造函数
constructor: Person,
name: 'liwens';
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // liwens
console.log(person2.name) // liwens
上面讲到构造函数通过 prototype 指向原型,实例通过 __proto__ 指向 原型
而原型是通过 constructor 属性指回构造函数的, 这样构造函数和原型都相互关联了
为了验证这一点,我们可以尝试:
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
我们再更新原型图
综上我们已经得出:
function Person() {
}
var person = new Person();
//实例通过 __proto__ 指向 原型
console.log(person.__proto__ == Person.prototype) // true
//原型通过 constructor 指向构造函数
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
读取一个实例的属性时,首先会在实例中找,如果找不到,就会根据 实例 的 __proto__ 属性去到原型中找,找不到就去原型的原型找,一直找到最顶层为止
function Person() {}
Person.prototype.name = 'liwens';
var person = new Person();
person.name = 'Daisy';
console.log(person.name); // Daisy
delete person.name;
console.log(person.name); // liwens
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。
但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype 中查找,幸运的是我们找到了 name 属性,结果为 Kevin。
但是万一还没有找到呢?原型的原型又是什么呢?
其实原型也是实例,他也有__proto__ 属性,我们可以输出看一下原型的原型是什么
function Person() {
}
console.log(Person.prototype)
可以看到,原型对象就是通过 Object 构造函数生成的,在 JavaScript 中, Object 是一切对象的源头
,所以我们再更新下关系图:
那 Object.prototype 的原型呢?
null,我们可以打印:
console.log(Object.prototype.__proto__ === null) // true
然而 null 究竟代表了什么呢?
引用阮一峰老师的 《undefined 与 null 的区别》 就是:
null 表示“没有对象”,即该处不应该有值。
所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。
所以查找属性的时候查到 Object.prototype 就可以停止查找了。
最后一张关系图也可以更新为:
顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
我们通篇介绍的原型链都是 es5 的写法,这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
所以 ES6 提供了更接近传统语言的写法
//ES5 写法
function Person() {
this.name = liwens;
}
Person.prototype.getName = function() {
return this.name;
};
//ES6 写法
class Person {
constructor() {
this.name = liwens;
}
getName() {
return this.name;
}
}
可以看到 ES6 class 写法更加清晰,也有封装的感觉。但 class 写法只是语法糖,也是基于 ES5 写法封装的。 我们还是很有必要学习原型链的原理的
更多内容可以阅读阮一峰老师 ECMAScript6 入门 - Class 的基本语法
最后补充几点大家可能不会注意的地方
constructor
首先是 constructor 属性,我们看个例子:
function Person() {}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 的值时,person 中并没有 constructor 属性,这属性是原型才有的,之所以还能取得。是因为
person.constructor === Person.prototype.constructor
**_**proto_****
其次是 _proto_ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj._proto_ 时,可以理解成返回了 Object.getPrototypeOf(obj)
。
真的是继承吗?
最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的 JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
原型是一个对象,一般以 Xxx.prototype 表示。构造函数通过 prototype 属性指向原型,而原型通过 constructor 属性指回构造函数,实例是通过 new 操作符调用构造函数生成的对象,每一个实例都带有_proto_属性 ,这个属性指向原型。
当我们想访问一个实例的属性时,首先会在实例本身找,找到就停止搜索并返回找到的值,找不到就通过_proto_访问原型找,如果还找不到就会去原型的原型找。一直到 Object, 在 JavaScript 中,一切对象的源头都是 Object
本文 参考自 冴羽的博客,通过 读 + 实践 + 自己理解写一遍。深刻理解其中原理。避免看过就忘
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.