认识原型链首先要认识原型的对象
JS中的对象有两个,一个普遍对象(Object),另一个是函数对(Function)。
而普通对象是通过对象字面量和使用new来创建对象的
一般来说,通过 new Function 产生的对象是函数对象,其他的都是普通对象。举例 说明:
function f1() { //函数声明
// todo
}
var f2 = function () { //函数表达式
// todo
};
var f3 = new Function('x', 'console.log(x)'); //new一个函数
var o1 = {}; //对象字面量创建对象
var o2 = new Object(); //new创建
var o3 = new f1();
console.log(
typeof f1, //function
typeof f2, //function
typeof f3, //function
typeof o1, //object
typeof o2, //object
typeof o3 //object
);
f1,f2再创建的时候,js会自动通过new function的方式创建他们,所以f123都是函数对象。
o12都是正常的创建对象。
但o3是new 一个函数对象来创建的对象,但是只有new function产生的对象才是函数对象,所以o3部署函数对象,它是普通对象。
认识原型链
function f(){}
var obj = new f()
在 JS 中,每当创建一个函数对象 f 时,该对象中都会内置一些属性,其中包括 prototype 和 proto, prototype 即原型对象,它记录着f的一些属性和方法。
需要注意的是,prototype 对 f 是不可见的,也就是说,f 不会查找 prototype 中的属性和方法。
比如
function f(){}
f.prototype.foo = "abc";
console.log(f.foo); // undefined
那么,prototype 有什么用呢? 其实 prototype 的主要作用就是继承。 通俗一点讲,prototype 中定义的属性和方法都是留给自己的 “后代” 用的,因此,子类完全可以访问prototype中的属性和方法。
function f() {}
f.prototype.foo = "abc";
var obj = new f();
console.log(obj.foo); // abc
其实呀,在new创造一个对象的时候,如上面,就会把父类的prototype赋值给新对象的—proto—属性,这样就是上面说的传承,因此子类才可以访问父类的prototype中的属性。
那父类的—proto—属性存的是谁的prototype呢,其实是object的属性
那这个传参的原理是什么呢,看下面原型链
其实在new一个新的对象的时候,会创建两条原型链,一条是上面这条obj的原型链,还有一条是 f 的原型链
f => Function.prototype => Obj.prototy
obj 对象拥有这样一个原型链以后,当 obj.foo 执行时,obj 会先查找自身是否有该属性,但不会查找自己的 prototype,当找不到foo时,obj 就沿着原型链依次去查找.
原型链的形成靠的是—proto—,而部不是prototype
认识函数作用域
function scope(){
var foo = "global";
if(window.getComputedStyle){
var a = "I'm if";
console.log("if:"+foo); //if:global
}
while(1){
var b = "I'm while";
console.log("while:"+foo);//while:global
break;
}
!function (){
var c = "I'm function";
console.log("function:"+foo);//function:global
}();
console.log(
foo,//global
a, // I'm if
b, // I'm while
c // c is not defined ,因为c在闭包里面,它有自己的作用域
);
}
scope();
scope函数中定义的foo变量,除过自身可以访问以外,还可以在if语句、while语句和内嵌的匿名函数中访问。 因此,foo的作用域就是scope函数体。
在javascript中,if、while、for 等代码块不能形成独立的作用域。因此,javascript中没有块级作用域,只有函数作用域和全局作用域。
但是,在JS中有一种特殊情况:
如果一个变量没有使用var声明,window便拥有了该属性,因此这个变量的作用域不属于某一个函数体,而是window对象。
作用域链
所谓作用域链就是:一个函数体中嵌套了多层函数体,并在不同的函数体中定义了同一变量, 当其中一个函数访问这个变量时,便会形成一条作用域链,如代码
foo = "window";
function first(){
var foo = "first";
function second(){
var foo = "second";
console.log(foo);
}
function third(){
console.log(foo);
}
second(); //second second->first->window,
third(); //first third->first->window
}
first();
当执行second时,JS引擎会将second的作用域放置链表的头部,其次是first的作用域,最后是window对象,于是会形成如下作用域链:
second->first->window, 此时,JS引擎沿着该作用域链查找变量foo, 查到的是 second
当执行third时,third形成的作用域链:third->first->window, 因此查到的是:frist
作用域链延长(with/catch)
with 和 catch 语句主要用来临时扩展作用域链, 将语句中传递的变量对象添加到作用域的头部。语句结束后,原作用域链恢复正常
//with语句
foo ="window";
function first(){
var foo ="first";
function second(){
var foo ="second";
console.log(foo);
}
function third(obj){
console.log(foo);//first
with (obj){
console.log(foo);//obj
}
console.log(foo);//first
}
var obj = {foo:'obj'};
third(obj);
}
first();
//catch语句
var e =new Error('a');
try {
throw new Error('b');
}catch (e) {
console.log(e.message);//b
}
在执行third()时,传递了一个obj对象,obj 中有属性foo, 在执行with语句时,JS引擎将obj放置在了原链表的头部,于是形成的作用域链如下:
obj->third->first->window, 此时查找到的foo就是obj中的foo,因此输出的是 obj
而在with之前和之后,都是沿着原来的链表进行查找,从而说明在with语句结束后,作用域链已恢复正常。看下面例子
var foo = "window";
var obj = {
foo : "obj",
getFoo : function() {
return function() {
return this.foo;
};
}
};
var f = obj.getFoo();
f(); //window 因为相当于只是执行了最里面那个函数,没有谁来调用
如果能把getFoo里面的第一层this存起来,放到第二层里面用,就是object
执行var f = obj.getFoo()返回的是一个匿名函数,相当于:
var f = function(){
return this.foo;
}
// f() 相当于window.f(), 因此f中的this指向的是window对象,this.foo相当于window.foo, 所以f()返回"window"
这里加一个关于js函数作用域和全局作用域有关的东西
下面代码就展示了js会有一个预解析。解析之后就会有一个语法树,然后再执行项执行之前把变量提升到作用域,这样也就是说只是声明了一下变量,没有赋值。
if (false) {
var log = "能否输出?"
}
console.log(log); // undefined 这里会有一个预解析(解析后会有一个语法树,再进入执行阶段)的过程,var 声明的变量会在执行前将变量提升到作用域上
if (false) {
let log1 = "能否输出?" // let 声明的变量只存在离他最近的大括号内
}
console.log(log1); // Uncaught ReferenceError: log1 is not defined,即使是 true log1 这行还是会报错
再如
使用 var 声明 for 循环的初始变量 j,最终 j 点击按钮的时候会输出计算后的最终值
使用 let 声明 for 循环的初始变量 j,由于存在块级别作用域,会将 j 的值锁死在当前作用域内,因此点击会输出对应的 j 值
for (let j = 0; j < 2; j++) {
document.querySelector('#let').addEventListener('click', () => console.log(j))
}
let 的写法明显优雅与闭包,但 let 声明变量性能会稍逊于 var,因为编译时会自动生成闭包