你不知道的Javascript(上)
该文章整理于书籍《你不知道的Javascript(上)》
第一章 作用域是什么
尽管通常将js归为“动态”或“解释执行”语言,但实际上它是一门编译语言。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。尽管如此,js引擎进行编译的步骤和传统编译语言非常相似,在某些环节可能比预想的要复杂。
传统编译语言在程序中的一段源代码在执行之前都会经历三个步骤,统称为“编译”。
- 分词/词法分析:这过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。
- 解析/语法解析:这个过程是将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(AST)
- 代码生成:将AST转换为可执行代码的过程,转化为一组机器指令。
引擎编译器作用域
- 引擎:从头到尾负责整个js程序的编译及执行过程
- 编译器:负责语法分析及代码生成等
- 作用域:负责收集并维护由所有声明的标识符组成的一系列拆线呢,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限
var a =2;
- 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,如果是,编译器会忽略该声明,继续进行编译,否则它会要求作用域在当前的作用域的集合中声明一个新的变量,并命名为a
- 接下来编译器会为引擎生成运行时所需的diamagnetic,这些代码被用来处理a=2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量。如果时,引擎就会使用这个变量,否则引擎会继续查找该变量。如果引擎最终找到a变量,就会将2赋值给它,否则引擎就会举手示意并抛出一个异常。
RHS和LHS
RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身。
- console.log(a) RHS
- a = 2; LHS
作用域嵌套
当一个块或函数嵌套在另一个块或函数中,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止。
异常
RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。
LHS如果在顶层中也无法找到目标变量,全局作用域中就会取创建一个具有该名称的变量,并将其返回给引擎,前提是程序运行在非严格模式下。严格模式也会抛出ReferenceError异常。
如果RHS找到一个比哪里,但是你尝试对这个变量的值进行不合理操作,则引擎也会抛出TypeError异常。
第二章 词法作用域
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另外一种叫动态作用域。
- 词法作用域关心函数和作用域时如何声明以及在何处声明的,是在写代码或者说定义时确定的,作用域链是基于代码中的作用域嵌套
- 动态作用域关心它们从何处调用,是在运行时确定的,作用域链是基于调用栈的
js所采用的作用域模型就是词法作用域,即由你在写代码时将变量和块作用域写在哪里来决定的。
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。作用域查找会在找到第一个匹配的标识符而停止。
全局变量会自动成为全局对象(比如浏览器的window对象)的属性,因此可以不直接通过全局对象的词法名称而是间接的通过全局对象属性的引用来对其进行访问。例如window.a。通过这种技术可以访问那些同名变量所遮蔽的全局变量,但非全局的变量如果被遮蔽了,无论如何都无法被访问到。
欺骗词法作用域会导致性能下降。js引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程种快速找到标识符。但如果引擎在代码中发现了欺骗词法,它只能简单的假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。最悲观的情况是所有的优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。
第三章 函数作用域和块作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
隐藏内部实现
如果所有的变量和函数都在全局作用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏最小特权原则(最小限度地暴露必要内容,而将其他内容都隐藏起来),因为可能会暴露过多的变量或函数,而这些变量或函数本身应该是私有的,正确的代码应该是可以组织对这些变量或函数进行访问的。隐藏作用域中的变量和函数所带来地另一个好处是可以避免同名标识符之间地冲突,两个标识符可能具有相同地名字但用途却不一样,无意间可能会造成命名冲突,冲突会导致变量的值被意外覆盖。
解决方法:
- 全局命名空间:这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。所有需要暴露给外界的功能都会成为这个对象的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
- 模块管理:从众多模块管理器中挑选一个来使用。使用这些工具任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定地作用域中。
函数作用域
在任意代码片段外部添加包装函数,可以将内部变量和函数定义隐藏起来,外部作用域无法访问包装函数内部地任何内容吗,但是存在着两个问题:
- 必须声明一个具名函数
- 必须显式调用这个具名函数才能运行其中地代码
解决:
(function foo(){
var a=3;
console.log(a);//3
console.log(foo);//函数
})();
console.log(a);//2
console.log(foo);//因为foo被绑定在函数表达式自身的函数中而不是外部作用域中,所以会报错
函数声明 和 函数表达式
区分二者最简单地方法是看function关键字出现在声明中地位置,函数声明以function开头,而函数表达式是(function开头。
匿名函数表达式
funtion()…没有名称标识符。函数表达式可以匿名,而函数声明则不可以省略函数名。缺点也有几点需要考虑:
- 匿名函数在栈追踪中不会显示出有意义的函数名,使调试很困难。
- 如果没有函数名,当函数需要引用自身时,只能使用已经过期的arguments.callee引用,比如在递归中。
- 匿名函数省略了对于代码可读性/可理解性很重要的函数名
立即执行函数表达式
由于函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数。
另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去
var a =2;
var blue = {
a:10,
}
function green(){
var a=5;
console.log(a);//5
}
green();
(function red(global){
var a =3;
console.log(a);//3
console.log(global.a);//10
})(blue);
(function red(global){
var a =3;
console.log(a);//3
console.log(global.a);//2
})(window);
(function red(global){
var a =3;
console.log(a);//3
console.log(global.a);//undefined
})(green);
console.log(a);//2
块作用域
尽管函数作用域是最常见的作用域单元,当然也是现行大多数js中最普遍的设计方法,但其他类型的作用域也是存在着。
for(var i=0;i<10;i++){
console.log(i);
}
console.log(i);
我们在for循环的头部直接i当以了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽略了i会被绑定在外部作用域(函数或全局)中的事实。开发者需要检查自己地代码,以避免在作用范围外意外地使用某些变量,如果在错误地地方使用变量将导致未知变量地异常。变量i的块作用域将使得其只能在for循环内部使用,如果在函数其他地方使用会导致错误。这对保证变量不会被混乱地复用及提升diamagnetic地可维护性都有很大地帮助。
es6引入了let关键字,提供了除var以外地另一种变量声明方式。通常let和{}花括号一起使用。
用let将变量附加在一个已经存在的块作用域上的行为是隐式的。在开发和修改代码的过程中,如果没有密切关注那些块作用域中有绑定变量,并且习惯性的移动这些块或者将其包含在其他块中,就会导致代码变得很混乱。
为块作用域显式的创建块可以部分解决这个问题,使变量的附属关系变得更加清晰。通常来讲,显式的代码优于隐式或一些精巧但不清晰的代码。显式的块作用域风格非常容易书写,并且和其他语言中块作用域的工作原理一致。只要声明有效,在声明中的任意位置都可以使用{…}括号来为let创建一个用于绑定的块。
应用场景
- 垃圾收集:块作用域可以让引擎清楚地知道没有必要继续保存
- let循环:let 不仅将i绑定到了for循环地块中,事实上它将重新绑定到了循环地每一个迭代中,确保使用上一个循环迭代结束时地值重新进行复制。
for(let i=0;i<10;i++){
console.log(i);
}
console.log(i);
let j;
for(j=0;j<10;j++){
let i=j;//每一次迭代都重新绑定
console.log(i);
}
当代码中存在对函数作用域中var声明地隐式依赖时,就会有很多隐藏地陷阱,如果用let来替代var则需要在代码重构的过程中付出额外的精力。
const
同样可以来创建块作用域变量,但其值时固定的厂里,之后任何试图修改值的操作都会引起错误。
块作用域替代方案
工具可以将ES6的代码转化成能在ES6之前环境中运行的形式,你可以使用块作用域来写代码,并享受它带来的好处,然后再构建时通过工具来对代码进行预处理,使之可以在部署时正常工作。
第四章 提升
变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2; 第一个声明事编译阶段进行的,赋值声明会被留在原地等待执行阶段。
注意:函数声明会被提升,而函数表达式不会被提升。
foo();
var foo = funtcion bar(){
…}
这个程序中的变量标识符foo()被提升并分配给了所在作用域(在这里是全局作用域),因此foo()不会导致ReferenceError。但是foo此时并没有复制(如果它是一个函数声明而不是函数表达式,就会赋值)。foo()由于对undefined值进行函数调用而导致非法操作,因此抛出TypeError异常。同时,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用。
普通块函数内定义函数尽量避免
var a = true;
console.log(foo);//undefined
foo();//TypeError: foo is not a function
if(a){
function foo(){console.log("a");}
}
console.log(foo);//函数
foo();//111
function foo(){
console.log("111");
}
注意:函数会首先被提升,然后才是变量
foo();//3
function foo(){
console.log(1);
}
var foo = function(){
console.log(2);
}
function foo(){
console.log(3);
}
实际上这个diamagnetic片段会被引擎理解为如下:
function foo(){
console.log(1);
}
function foo(){//出现在后面的函数声明还是可以覆盖前面的
console.log(3);
}
//var foo;是重复的声明因此会被忽略
foo();
foo = function(){
console.log(2);
}
第五章 作用域闭包
资料整理于JavaScript从入门到放弃 第十章 深入理解闭包
作用域:全局作用域和函数作用域
闭包:定义在一个函数内部的函数,它能够记住诞生的环境,可以读取其作用域里的变量。通过不同方式对函数类型的值进行传递,可以在别处外部访问其内部的变量。本质上闭包就是函数内部和函数外部链接的一座桥梁,是一个函数被当作一个值传递后,脱离了原作用域在新的作用域被引用后依然保持以前作用域能访问的部分。
闭包产生条件:1、函数嵌套 2、访问所在作用域 3、在所在作用域外被调用
闭包作用
一、读取函数内部的变量,这些变量始终在内存中,使用闭包小心内存的泄漏
//计数器
function a(){
var start = 0;
console.log(start);
function b(){
return start++;
}
return b;
}
// a();//0
// a();//0
// a();//0
var inc = a();//0 //每个父函数调用完成,都会形成新的闭包,父函数中的变量始终都会在内存中,相当于缓存,小心内存泄漏
// inc();//无
// inc();//无
// inc();//无
// console.log(a());//0 函数b
// console.log(a());//0 函数b
// console.log(a());//0 函数b
// console.log(inc());//0
// console.log(inc());//1
// console.log(inc());//2
inc = null; //释放内存,小心内存泄漏
二、能够封装对象的私有属性和方法
function Person(name){
//私有属性
var age;
//私有方法
function setAge(n){
age = n;
}
function getAge(){
return age;
}
return {
name:name,
setAge:setAge,
getAge:getAge,
}
}
var p1 = Person('lily'); //每个父函数调用完成,都会形成新的闭包,父函数中的变量始终都会在内存中,相当于缓存,小心内存泄漏
p1.setAge(18);
console.log(p1.getAge());//18
p1 = null;//释放变量
1、 模块
模块模式需要具备的两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
模块模式的优点:
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行i需改,包括添加或删除方法和属性,以及修改它们的值。不用模块化的情况下,模块内部修改一个方法名字,那样所有外部引用这个方法名的地方都要改,而用了模块化后,对外方法名不变,外部调用这个方法名的地方都不要变,而要模块内部对原有函数新增或修改之后,再像上面再模块内部对外暴露方法名的地方给方法名重新赋值即可。这样改动模块外是无法感知的。
var MyModule = (function Manager(){
var modules = {};
function define(name,deps,impl){
for(var i=0;i<deps.length;i++){
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl,deps);
}
function get(name){
return modules[name];
}
return {
define:define,
get:get
}
})()
MyModule.define("bar",[],function(){
function hello(who){
return "Let me introduce:"+who;
}
return {
hello:hello
}
});
MyModule.define("foo",["bar"],function(bar){
var hungry = "hippo";
function awesome(){
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome:awesome,
};
});
var bar = MyModule.get("bar");
var foo = MyModule.get("foo");
console.log(bar.hello("hippo"));//Let me introduce:hippo
foo.awesome()//LET ME INTRODUCE:HIPPO
MyModules
|_ _ bar
| |_ _ hello(who)
| _ _ foo
|_ _awesome()
以上代码运行后如结构图所示:MyModules是个母模块,下面有两个子模块,分别实现了自己的方法。
然后解释一下母模块,MyModules提供了两个方法,define可以给用户定义自己的模块,get获取已有的模块。重点解释一下define。define有三个参数:name :要定义模块的名字,deps:模块所要依赖的,impl接口:实现的过程;
然后直接看foo子模块的定义: MyModules.define(“foo”, [“bar”], function(bar) { … },这里比较模糊的是第二个参数 ["bar”] 有什么作用。在foo 的实现中,需要调用bar的方法hello,因此需要把模块bar传入,但是,bar是在哪里实现的?在MyModules实现的,我们需要提供一个参数来告诉MyModules帮我们把bar注入到foo中,这就是define函数第二个参数的用法。
回到define的实现代码中:for (var i=0; i<deps.length; i++) {deps[i] = modules[deps[i]];}, 还记得第二个参数的作用吗?[‘bar’],它是告知MyModules我所要依赖的子模块,但这个参数是个数组,并且里面的元素’bar’是字符串,所以这段代码的功能是:遍历deps,并把字符串换成模块,也就是[‘bar’] 变成了[MyModules.bar]
回到最重要的一段代码:modules[name] = impl.apply(impl,deps),这个的用法是把[MyModules.bar] 注入到 第三个参数impl : function(bar) { … } 的实现中,然后this仍然指向自身,并把该函数赋值给modues。
总结:这种实现既保证了各个子模块的封闭性,又不缺乏可扩展性,很值得学习
2、 未来模块机制
再通过模块系统进行加载时,ES6会将文件当作独立的模块来处理,每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。
bar.js
function hello(who){
return "Let me introduce:"+who;
}
export hello
foo,js
import hell from "bar"
var hungry = "hippo";
function awesome(){
console.log(hungry).toUpperCase());
}
export awesome;
baz.js
module foo from "foo";
module bar from "bar";
console.log(bar.hello("riho"));//Let me introduce riho
foo.awesome();//LET ME INTRODUCE HIPPO
- import 可以将一个模块中的一个或多个API导入到当前的作用域中,并分别绑定在一个变量上。
- module会将整个模块的API导入并绑定在一个变量上
- export会将当前模块的一个标识符(变量,函数)导出未公共API。
import和export编译成es5其实就是一个个的IIFE生成的单例模块。
闭包问题
使用闭包使得函数中的变量始终在内存中,内存消耗很大,所以不能滥用闭包,否则会造成页面的性能问题。
function a(){
var start = 0;
function b(){
return start++;
}
return b;
}
var inc = a();//每个父函数调用完成,都会形成新的闭包,父函数中的变量始终都会在内存中,相当于缓存,小心内存泄漏
console.log(inc());//0
console.log(inc());//1
console.log(inc());//2
console.log(a()());//函数用完即释放,不会保存start,所以一直都是0
console.log(a()());//0
console.log(a()());//0
inc = null;
IIFE立即执行函数
定义函数之后,立即调用该函数
//计数器
//自定义属性
function add(){
return ++add.count;
}
//容易被全局无意修改
add.count=0;
add.name1='wu';
console.log(add.name1);//'wu'
console.log(add.count);//0
console.log(add());//1
console.log(add());//2
console.log(add());//3
//通过如下IIFE这样可以减少对全局变量的污染,同时可以避免自定义属性被外部无意修改
var a = (function(){
var count = 0;
function b(){
return ++count;
}
return b
})()
console.log(a());//1
console.log(a());//2
console.log(a());//3
a = null;
循环闭包
应用场景:可能有点击事件会需要在循环中获取每个循环的i
//【未用到闭包结果无意修改】
//原因:无函数嵌套
//会很容易受外部无意修改,因为没有函数外部调用函数
function foo(){
var arr= [];
for(var i=0;i<10;i++){
arr[i] = i;
}
return arr;
}
var bar = foo();
console.log(bar[8]);//8
bar[8] = 10;
console.log(bar[8]);//10
//【用到闭包但结果有问题】
//十次for循环是同步的,赋值是异步的。先同步后异步,所以异步的时候i已经是10。
//在执行栈中不断执行知道i=10,执行栈未空开始传递i=10给消息队列中十个赋值。
//问题:我们试图假设循环中每个迭代在运行时都会给自己捕获一个i的副本,但是根据做音乐的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
function foo(){
var arr= [];
for(var i=0;i<10;i++){
arr[i] = function(){
return i
};
}
return arr;
}
var bar = foo();//因为arr[i]是函数,所以当函数调用的时候实际上循环已经结束,此时i为10,再去调用不管任意索引i都为10
console.log(bar[0]());//10
//【有闭包结果正确】通过立即执行函数,使得训话过程中每个迭代都创建一个新的闭包作用域
//若没有传参,直接使用i则不行,因为var在声明变量时若没有这个变量则创建新的,若存在就跳过这一步直接赋值,也就是说取到的i都是同一个
function foo(){
var arr= [];
for(var i=0;i<10;i++){
arr[i] = (function(n){
//此时通过建立了一个函数作用域传递i给n,使得该作用域隔绝外界,同时该作用域只保留传递的i的内存
return function(){
return n;
};
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[1]());//1
//【不含闭包但结果无意修改】
//原因:以下虽然能实现计数器,但是虽有函数嵌套,没有函数外部调用函数,不含闭包会很容易受外部无意修改
function foo(){
var arr= [];
for(var i=0;i<10;i++){
arr[i] = (function(){
return i;
})();
}
return arr;
}
var bar = foo();
console.log(bar[8]);//8
bar[8] = 10;
console.log(bar[8]);//10
//【用到闭包 结果没问题】
function foo(){
var arr= [];
for(var i=0;i<10;i++){
(function(n){
arr[i] = function(){
return n;
};
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[8]());//8
//【未用到闭包结果无意修改】
//原因:因为没有函数外部调用函数
function foo(){
var arr= [];
for(var i=0;i<10;i++){
(function(n){
arr[i] = n;
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[8]);//8
bar[8] = 10;
console.log(bar[8]);//10
//【使用闭包结果正确】ES6 let
function foo(){
var arr= [];
for(let i=0;i<10;i++){//let表明这个括号产生块作用域
arr[i] = function(){
return i;//此时内存中的i是这个块作用域中的内存所保存的每个循环中的i
}
}
return arr;
}
var bar = foo();
console.log(bar[8]());//8
bar = null;
闭包应用
场景一:返回值
function a(){
var i = 0;
return function(){
return i;
}
}
var c = a();
console.log(c());
c = null;
场景二:函数赋值
//将内部函数赋值给一个外部变量
var d;
var a = function(){
var i = 0;
var b = function(){
return i;
}
d = b;
}
a()
console.log(d());
a = null;
场景三:函数参数
//把函数当成另一个函数的参数
function fn2(n){
console.log(n());//ww
}
function fn(){
var name = 'ww';
var a = function(){
return name;
}
fn2(a);
}
fn();
场景四:IIFE
function fn2(n){
console.log(n());//ww
}
(function fn(){
var name = 'ww';
var a = function(){
return name;
}
fn2(a);
})()
场景五:循环赋值
function foo(){
var arr= [];
for(var i=0;i<10;i++){
arr[i] = (function(n){
//此时通过建立了一个函数作用域传递i给n,使得该作用域隔绝外界,同时该作用域只保留传递的i的内存
return function(){
return n;
};
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[1]());//1
bar = null;
场景六:getter和setter
//封装私有变量和方法
var getValue,setValue;
(function(){
var num=0;
getValue = function(){
return num;
}
setValue = function(v){
num = v;
}
})()
console.log(getValue());
setValue(10);
console.log(getValue());
场景七:迭代器 计数器
var arr = ['red','blue','green'];
var a = (function(arr){
var count=0;
return function(){
return arr[count++];
}
})(arr)
console.log(a());//red
console.log(a());//blue
console.log(a());//green
a = null;
场景八:区分首次
var firstLoad = (function(){
var list =[];
return function(id){
if(list.indexOf(id)>=0){
//list已经有id
return false;
}
else{
list.push(id);
return true;
}
}
})()
console.log(firstLoad(10));//true
console.log(firstLoad(10));//false
console.log(firstLoad(11));//true
firstLoad = null;
场景九:缓存机制
//无缓存机制
function mult(){
var sum = 0;
for(var i = 0;i<arguments.length;i++){
sum = sum+arguments[i];
}
return sum;
}
console.log(mult(1,2,3,4,5));
console.log(mult(1,2,3,4,5));
//有缓存机制
//模拟对象的key,看该对象中是否有相同的key,如果有直接获取val返回
var mult = (function(){
var cache = {};
var calculte = function(){
var sum = 0;
for(var i = 0;i<arguments.length;i++){
sum = sum+arguments[i];
}
return sum;
}
return function(){
//对cache对象进行操作
var args = Array.prototype.join.call(arguments,',');
console.log(cache);
if(args in cache){
return cache[args];
}
return cache[args] = calculte.apply(null,arguments)
}
})()
console.log(mult(0,2,3,4,5));//14
console.log(mult(1,9,3,4,5));//22
console.log(mult(1,2,3,4,5,6));//21
场景十:img对象图片上报
//new Image()进行数据上报
var report = function(src){
img = new Image();
img.src = src;
}
report("http://www.moontang.xyz")
//低版本浏览器在进行数据上报会丢失30%左右的数据
var report = (function(src){
var imgs = [];
return function(){
var img = new Image();
imgs.push(img);
img.src = src;
}
})();
report("http://www.moontang.xyz")
console.log(report);
第六章 关于this
this提供了一种更优雅的方式隐式传递一个对象,因此可以将API设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来愈混乱,使用this则不会这样。
两个误解
一、指向自身
function foo(num){
console.log("foo"+num);
this.count++;
}
foo.count=0;
var i;
for(i=0;i<10;i++){
if(i>5){
foo(i);
}
}
console.log(count);//NaN
console.log(undefined+1);//NaN
console.log(foo.count);//0
执行foo.count=0时的确向函数对象foo添加了一个属性count,但是函数内部代码this.count中得this并不是指向那个函数对象。实际上,这段代码在无意中创建了一个全局变量count,它的值一开始undefied,但是自加以后变为了NaN。
二、它的作用域
在js内部,作用域和对象类似,可见的标识符都是它的属性,但是作用域的对象无法通过js代码访问,它存在于js引擎内部。
//指向作用域
function foo(){
var a =2;
bar();
}
function bar(){
console.log(this.a);//undefined
}
foo();
this和词法作用域查找混合使用时,无法实现。
定义
this是在运行时绑定的,当一个函数被调用时会创建一个活动记录。这个记录会包含函数在哪里被调用(调用栈),函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。不同的调用主体会创建不同活动记录,this一定程度上可以认为是这个调用主体的象征。这个地方函数不包括箭头函数,箭头函数中this的指向是箭头函数定义时this指向,不是箭头函数调用时上下文。
默认绑定(独立调用)
1、全局环境下this指向window
console.log(this);//window
2、函数独立调用,函数内部的this也指向window
严格模式下,独立调用的函数内部this指向了undefined
function foo(){
console.log(this);//window
}
foo();
3、被嵌套的函数独立调用时,this默认指向了window
var obj ={
foo:function(){
function test(){
console.log(this);//window
}
test();
}
}
obj.foo();
4、自执行函数中内部this指向window
var a = 10;
function foo(){
console.log(this);//obj
(function(that){
console.log(this);//window
console.log(that.a);//2
})(this)
}
var obj = {
a:2,
foo:foo
}
obj.foo();
5、闭包this默认指向window
var a = 0;
var obj = {
a:2,
foo:function(){
var c = this.a;//2
var that = this
return function test(){
console.log(this);//window
console.log(that);//obj
return that.a//2
}
}
}
var fn = obj.foo()
console.log(fn());//2
隐式绑定(方法调用)
在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上
function foo(){
console.log(this.a);
}
var obj = {
a:1,
foo:foo,
obj2:{
a:2,
foo:foo
}
}
obj.foo();//1
obj.obj2.foo();//2
隐式丢失
隐式丢失指的是被隐式绑定的函数丢失了绑定对象,从而默认绑定到window。这种情况比较容易出错却又非常常见。
1、函数别名
var a = 0;
function foo(){
console.log(this.a);
}
var obj = {
a:1,
foo:foo
}
var bar = obj.foo;
bar();//0
//等价于
var a =0;
var bar = function foo(){
console.log(this.a);
}
bar();//0
2、参数传递
因为js是基于函数作用域的,js中的对象没有作用域的概念。
比如:全局var num = 9; var obj = {a:2,fn:function(){}};
函数fn的作用域是全局对象,你可以在fn中访问num,但是不能访问到a,也就是说函数fn的作用域链上并不包含obj对象,如果要访问a,只能在fn里面使用this.a来访问,并且对函数fn的调用方式是obj.fn();
var a =0;
function foo(){
console.log(this.a);
}
function bar(fn){
fn();
}
var obj = {
a:1,
foo:foo
}
//把obj.foo当作参数传递到bar函数中,有隐式的函数赋值 fn = obj.foo,指示把foo函数赋值给了fn,而
//fn与obj对象毫无关系,所以当前foo函数内部的this指向了window
bar(obj.foo);//0
//等价于
var a = 0;
function bar(fn){
fn();
console.log(this);//window
}
bar(function foo(){
console.log(this.a);//0
})
3、内置函数
var a = 10;
setTimeout(function(){
console.log(this);//window
},1000)
function foo(){
console.log(this.a);
}
var obj = {
a:1,
foo:foo
}
setTimeout(obj.foo,1000);//10
4、间接调用
function foo(){
console.log(this.a);
}
var a = 2;
var obj = {
a:3,
foo:foo
}
var p ={a:4};
obj.foo();//3
//将obj.foo函数对象赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即调用,内部的this默认指向window
(p.foo=obj.foo)();//2
//将obj.foo赋值给p.foo函数,之后p.foo()函数再执行,其实是属于p对象的方法的指向,this指向了当前的p对象
p.foo();//4
5、其他
var a = 0;
var obj = {
a:1,
foo:foo
}
function foo(){
console.log(this.a);
}
(obj.foo=obj.foo)();//0
(false || obj.foo)();//0
(1,obj.foo)();//0
显式绑定(间接调用)
可以使用call() apply() bind()等方法。它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this,因为你可以直接指定this的绑定对象。但是这种依然无法解决隐式丢失问题,显式绑定在函数被调用时触发,但你无法确定当你把函数作为参数传给其他函数后其他函数会以何种方式来调用这个而函数。除非在函数被传递之前手动显式绑定一下,也就是下面的硬绑定。
严格模式下,函数apply()和call()内部的this始终是它们的第一个参数
如果传入一个原始值(字符串类型、布尔类型或数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式。这通常被称为“装箱”。把基本数据类型转换为对应的引用类型的操作称为装箱,把引用类型转换为基本的数据类型称为拆箱。
//显式绑定
//call apply bind
var a = 0;
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
foo();//0
foo.call(obj);//2
foo.apply(obj);//2
(foo.bind(obj))();//2
1、硬绑定
创建函数在内部手动调用foo.call(obj),因此强制把foo的this绑定到obj,无论之后如何调用函数bar,它总会手动在obj上调用window。这种绑定是一种显式的强制绑定,因为我们称之为硬绑定。
var a =0;
function foo(){
console.log(this.a);
}
var obj = {
a:2,
}
var bar = function(){
foo.call(obj)
console.log(this.a);//0
}
bar();//2
setTimeout(bar,2000);//2
bar.call(window);//2 0
应用场景
① 创建一个包裹函数,负责接收参数并返回值
function foo(something){
console.log(this.a,something);//2 3
return this.a + something;
}
var obj = {
a:2
}
var bar = function(){
return foo.apply(obj,arguments);
}
var b = bar(3);
console.log(b);//5
② 创建一个可以重复使用的辅助函数
var obj = {
a:2
}
function foo(something){
console.log(this.a,something);//2 3
return this.a + something;
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
}
}
var bar = bind(foo,obj);
var b = bar(3);
console.log(b);//5
2、API调用的“上下文”
//许多内置函数都有显式绑定的作用
//数组的额forEach(fn,对象) map() filter() some() every()
var id = "window"
function fn(el){
console.log(el,this.id);
}
var obj = {
id:'fn'
}
var arr = [1,2,3];
// arr.forEach(fn);//window
// arr.forEach(fn,obj);//fn
arr.forEach(function(el,index){
console.log(el,index,this.id);//fn
},obj)
new绑定(构造函数)
function fn(){
//如果是new关键字来执行函数,相当于构造函数来实例化对象,那么内部的this指向了当前实例化的对象
console.log(this);//fn对象
//return ;//相当于返回fn对象本身
}
var fn_now = new fn();
console.log(fn_now);//fn对象
//--------------------------------
function fn2(){
//this还是指向了当前的对象
console.log(this);//fn2对象
//使用return关键来返回对象的时候,实例化出来的对象是当前的返回对象
return {
name:'tang'
}
}
var fn2_now = new fn2();
console.log(fn2_now);//return里的对象{name:'tang'}
//--------------------------------
function fn3(){
//this还是指向了当前的对象
console.log(this);//fn3对象
//使用return关键来返回对象的时候,实例化出来的对象是当前的返回对象
return this;
}
var fn3_now = new fn3();
console.log(fn3);//fn3函数
console.log(fn3_now);//fn3对象
//--------------------------------
var person = {
fav:function (){
return this;
}
}
//实例化出来的对象内部的属性constructor属性指向了当前的构造函数
var p = new person.fav();
console.log(p,p===person);//fav{} false
console.log(p.constructor===person.fab);//true
优先级
new > 显式绑定 > 隐式绑定 > 默认绑定
绑定例外
1、被忽略的this
如果把null或者undefined作为this的绑定对象传入call,apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null);//2
一种非常常见的做法是使用apply()来展开一个数组,并当作参数传入一个函数,类似的,bind()可以对参数进行柯里化(预先设置一些参数),这种方法非常有用。
柯里化:把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
function foo(a,b){
console.log("a:"+a+",b:"+b);
}
//把数组展开成参数
foo.apply(null,[2,3]);//a:2,b:3
//使用bind进行柯里化
var bar = foo.bind(null,2);
bar(3)//a:2,b:3
如果并不关心this的话,但仍然需要传入一个占位值,这时null可能是一个不错的选择。然而总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象,这将导致不可预计的后果。显而易见这种方法可能会导致许多难以分析和追踪的bug。
解决:Object.create(null)创建DMZ空对象,然后用空的非委托对象替代null
2、软绑定
硬绑定这种方式可以把this强制绑定到指定的额对,防止函数调用应用默认绑定规则。
如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。
ES6箭头函数
箭头函数不适用this四种标准规则,而是根据外层(函数或全局)作用域来决定this
var a = 1;
function foo(){
var self = this;
console.log(self);//obj1;
return (a)=>{
//这个地方的this是跟随foo的this,相当于self = this;将self用于箭头函数内部this
console.log(this);//obj1
console.log(this.a);//2
}
}
var obj1 ={
a:2
}
var obj2 ={
a:3
}
var bar = foo.call(obj1);
bar();
//结果同上
//bar.call(obj2);
var a = 1;
function foo(){
return function(){
console.log(this);//window
console.log(this.a);//1
}
}
var obj1 ={
a:2
}
var bar = foo.call(obj1);
bar();
虽然self=this和箭头函数看起来都可以取代bind,但是从本质上来说,它们想替代的是this机制。如果经常编写this风格的代码,但是绝大部分的时候都会使用self=this或箭头函数来否定this机制,那你或许应当:
- 只使用词法作用域并完全抛弃错误this风格的代码
- 完全采用this风格,在必要时使用bind(),尽量避免使用self=this和箭头函数。
当然,包含这两种代码风格的程序可以正常运行,但是在同一个函数或同一个程序中混合的使用这两种风格通常会使代码更难维护,并且可能也会更难编写。
第七章 对象
对象定义方式
声明形式和构造形式。
二者生成的对象是一样的,唯一的区别是在文字声明中你可以添加多键值对,但是在构造形式中你必须逐个添加属性。
//声明
var myobj = {
key:value,
};
//构造形式
var myobj = new Object();
myobj.key = value;
类型
JavaScript共有八种数据类型,分别是 undefined、null、boolean、number、string、object、Symbol、BigInt。其中 Symbol 和 BigInt 是ES6 中新增的数据类型。
Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
BigInt是一种数字类型数据,它可以表示任意精度格式的整数,使用BigInt可以安全存储和操作大整数,即使这个数超出了number能够表示的安全整数范围。
注意简单基本类型(undefined、null、boolean、number、string)本身并不是对象。有一种常见错误说法是“js中万物皆对象”这显然是错误的。对象分为许多对象子类型,我们可以称之为复杂基本类型,比如函数和数组。
js中还有一些对象子类型,通常被称为内置对象,有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂。比如:String、Number、Boolean、Object、Function、Array、Date、Error(抛出异常被自动创建)、RegExp(正则表达式)。这些内置函数可以当作构造函数(由new产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
原始值“i am a string" 并不是一个对象,它指示一个字面量,并且是一个不可变的值,如果是在这个字面量上执行一些操作,那需要转化成String对象。语言会自动(装箱)把字符串字面量转化成一个String对象。
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造。没有文字形式。
null有时会被当作一种对象类型,但这其实是语言本身的一个bug,即对null执行typeof null返回object,实际上,null本身是基本类型。原理是这样的,不同的对象在低层都表示为二进制,在js中二进制前三位为0的话会被判断为object类型,null的二进制表示是全0,自然前三位为0,所以typeof会返回object。
内容
对象的内容是由一些存储在特定命名位置的值组成的,我们称之为属性。但是在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置。
原始数据类型存储在栈中,引用数据类型存储在对中,在栈中存储了指针。该指针指向实体的起始地址,当寻找引用值时,会首先检索其在栈中的地址,然后从堆中获得实体。
访问方式:
- .a 语法称为属性访问(命名符合标识符规范)
- [“a”] 语法称为键访问(任意UTF-8/Unicode字符串)
注意:属性名永远是字符串,如果使用string(字面量)以外的其他值作为属性名,那它首先会被转换成一个字符串。但是在ES6中,引入了一种新的数据类型map,也是一种键值对集合,但其键可以是任何数据,不仅限于string。
可计算属性名
只可以通过键访问实现可计算属性名。
var prefix = "foo"
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]:'world',
}
console.log(myObject.foobar);//hello
console.log(myObject["foobar"]);//hello
console.log(myObject[prefix + "bar"]);//hello
console.log(myObject.prefix+bar);//ReferenceError: bar is not defined
属性和方法
函数永远不会属于一个对象。
- 有些函数具有this引用,但this是在运行时根据调用位置动态绑定的,所以函数和对象的关系最多也只能说是间接关系。
- 如果属性访问返回的是一个函数,那它也并不是一个方法。属性访问返回的函数和其他函数并没有任何区别
- 同一个函数的不同应用,并不能说明这个函数是特别的或者属于某个对象
- 即使在对象的文字形式中声明一个函数表达式,这个函数也不会属于这个对象,因为它们只是对于相同函数的多个引用。对象中的函数不是只属于这个对象的,对象中的函数只是函数的引用。除非通过硬绑定将函数this对象绑定为对象,那么每次执行函数时都是绑定对象在调用函数。
数组
数组本质也是对象子类型,只不过其键是数值下标。
虽然每个下标都是整数你仍然可以给数组添加属性:
- 添加命名属性,数组的length不变
- 添加一个属性看起来像数字,它会变成数值下标,数组的length会变
复制对象
1、浅拷贝:只复制引用,没有复制真正的值。
//基本数据类型
//当我们申明一个基本类型并对它进行赋值的时候,计算机会将值保存在栈内存中。
var str1 = 'shen'
var str2 = str1
str2 += 'zhiyong'
console.log('str1:', str1) //shen
console.log('str2:', str2) //shenzhiyong
//引用数据类型
//而当我们申明一个引用数据类型并对它进行赋值的时候,计算机会将值保存在堆内存中,引用类型变量其实就是一个指针指向堆内存中。如果复制两相同的引用类型变量,其实它们最终指向同一个对象或者说堆内存空间。
var obj1 = {
name: 'shen'
}
var obj2 = obj1
obj2.name = 'shenzhiyong'
console.log('obj1:', obj1) // obj1: {name: "shenzhiyong"}
console.log('obj2:', obj2) // obj2: {name: "shenzhiyong"}
ES6定义了Object.assign(…)方法来实现千夫指,第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举并把它们复制(使用=)到目标对象,最后返回目标对象。
2、深拷贝:非复制引用。
var obj1 = {
name: 'shen'
}
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = 'shenzhiyong'
console.log('obj1:', obj1) // obj1: {name: "shen"}
console.log('obj2:', obj2) // obj2: {name: "shenzhiyong"}
JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙地赋值方法:JSON.parse(JSON.stringify(obj1))。安全是指属性不丢失,有时候null,undefined和函数都不会转换即发生可丢失。缺点是当对象里面出现函数的时候就不适用了。
属性描述符
在创建普通属性时属性描述符会使用默认值,这些属性描述符可以直接检测出属性特性方法。
- Writable决定是否可以修改属性的值
- Configurable修改属性描述符
- Enumerable是否可枚举访问
var myObject = {};
Object.defineProperty(myObject,"a",{
value:2,
writable:true,
configurable:true,
enumerable:true
})
不变性
有时会希望属性或对象是不可改变的,在ES5中可以通过很对方法实现。
很重要的一点是,所有的方式创建的都是浅不变性,也就是说它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数等),其他对象的和内容不受影响,仍然是可变的。
myObject.foo;//[1,2,3]
myObject.foo.push(4);//[1,2,3,4]
myObject.foo;//[1,2,3,4]
假设代码中myobject已经被创建且是不可变的,但是为了保护其内容,你还需要使用下面的方法让foo也不可变:
- 对象常量:writable和configurable为false
- 禁止扩展:不许增删(Object.preventExtensions(myobject))
- 密封:不许增删,不许重新配置属性(Object.sela(…))
- 冻结:不许增删,不许重新配置属性,不许修改属性值(Object.freeze(…))。这时可以应用在对象上级别最高的不可变性。其还会影响vue2的响应式绑定。
[[Get]]
- myobject.a是一次属性访问,实际上是实现了一次[[Get]]操作。
- 如果没有是找到名称相同的属性,按照其算法定义会遍历原型链。
- 如果无论如何都找不到名称相同的属性,那么会返回undefined(对象会返回undefined,词法作用域会返回ReferenceError)
[[Put]]
如果对象中已经存在这个这个属性,其算法会检查以下:
- 属性是否访问描述符,如果是并且存在setter就调用setter
- 属性的数据描述符中writable是否为false?是则在非严格模式下静默失败,在严格模式下抛出TypeError异常。
- 如果都不是,将该值设为属性的值
Getter和Setter
二者是隐藏函数。getter会在获取属性值的时候调用,setter会在设置属性值时调用。
当给一个属性定义getter\setter或二者都有时,这个属性会被定义为访问描述符。js会忽略它们的value和writable,取而代之关心set和get特性,其二者优先级更高。
var myobject = {
get a(){//只能堆属性a进行操作
return this._a_;//变量名和方法名重复会导致死循环
//return 2;//set操作会忽略赋值操作无意义。
},
set a(val){//只能对属性a进行操作
this._a_ = val*2;
}
}
//只能堆属性a进行操作
myobject.a = 2;
console.log(myobject.a);//4
myobject.b = 3;
console.log(myobject.b);//3
存在性
背景:属性访问返回值为undefined无法区分究竟是属性中存储undefined还是属性不存在。
解决:我们可以在不访问属性值的情况下判断对象中是否存在这个属性。
- in检查:对象+原型链
- hasOwnProperty:对象。还可以通过显示绑定进行判断。
注意:
- 看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别十分重要,4 in [2,4,6]的结果并不是true,因为[2,4,6]这个数组中包含的属性名是0,1,2没有4。
- 在数组是应用for … in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举(enumerable)属性。最好只在对象上应用for…in循环,遍历数组就使用传统的for循环来遍历数值索引。
- for…in可枚举,对象+原型链
- Object.keys(myobject)可枚举,只有对象
- Object.getOwnPropertyNames(myobject)所有属性,只有对象
遍历
- 数值索引数组:标准for循环【遍历下标来指向值】
- forEach:遍历数组所有值并忽略回调函数返回值
- every:会一直运行到回调函数返回false
- some:会一直运行到回调函数返回true
- for…in:遍历对象无法直接获取属性值,因为它遍历的是对象中的的所有可枚举属性,需要手动获取属性值
- for…of:循环访问对象,直接获取属性值
//对象遍历
//forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。
//注意: forEach() 对于空数组是不会执行回调函数的。
var sum = 0;
var arr = [1,2,3,4,10];
arr.forEach(function(item){
sum = sum + item;
})
console.log(sum);//20
//every用于检测数组中所有元素是否都符合指定条件
//如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
//如果所有元素都满足条件,则返回 true。
var ages = [32,33,16,40];
var isabove = ages.every(function(item){
return item >18;
})
console.log(isabove);//false
//some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。
//如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
// 如果没有满足条件的元素,则返回false。
var ages = [32,33,16,40];
var isabove = ages.some(function(item){
return item >18;
})
console.log(isabove);//true
//for..in可枚举
//在数组上应用for...in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。
//最好在对象上用for...in,在数组上使用传统for循环遍历数值索引。在对象上无法直接获取属性值,因为它实际上遍历的时对象中所有可枚举属性,需要手动获取属性值
var obj = {
"a":2,
"b":3,
"c":4,
"d":"dd",
}
for (var k in obj){
console.log(k,obj[k]);
}
var arr = [4,5,6,7,8];
for (var k in arr){
console.log(k,arr[k]);//0 4 ; 1 5 ; 2 6 ; 3 7 ; 4 8;
}
//for..of遍历值而不是下标或者对象属性
//会寻找内置或自定义的@@iterator对象并调用它的next()方法来遍历数据值。
//首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回的值
for (var k of arr){
console.log(k);//4 5 6 7 8
}
注意:
- 遍历数组下标采用的是数字顺序,但是遍历对象属性的顺序是不确定的。
- 关于for…of原理:循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象next()方法来遍历所有返回值。数组内有内置的@@iterator因此for…of可以直接应用在数组上。但是普通的对象没有内置的@@iterator,所以无法自动完成for…of遍历。但是你可以给任何想遍历的对象定义@@iterator。
第八章 混合对象“类”
类理论
面向对象编程:强调数据和操作数据的行为本质上是相互关联的。因此好的设计就是把数据和它相关的行为打包(或封装)起来。
类继承和实例化,另一个核心概念是多态,这个概念是说父类的通用行为可以被子类用更特殊的行为重写。类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。
js只有一些近似类的语法元素(比如new和instanceof),不过后来ES6新增了一些元素,比如class关键字。但实际上js中实际上并没有类。
function Person(name){
if(this instanceof Person){
this.name = name;//this指向当前实例,外部使用关键字new
}
else return new Person(name);//this指向window,外部未使用关键字new
}
var p1 = new Person('xiaohua');
console.log(p1.name);//xiaohua
var p2 = Person('xiaohong');
console.log(p2.name);//xiaohong
虽然js中有近似类的语法但是js机制似乎一直在组织使用类设计模式。在近似类的表象下,js的机制其实和类完全不同。
类的机制
在许多面向类的语言中,标准库会提供Stack类,他是一种栈数据结构,其内部会有一些变量来存储数据,同时会提供一些公有的可访问行为,从而让代码可以和数据进行交互。但是实际上并不是直接操作Stack,Stack类仅仅是一个抽象的表示,它描述了所有栈需要做的事,但它本身并不是一个栈,你必须先实例化stack类然后才能对它进行操作。
类和实例对象之间的关系看作是直接关系而不是间接关系更有助于理解。类通过复制操作被实例化为对象形式。这里复制非引用,用构造函数new一个实例,构造函数返回的是对象,这个对象指向创建的这个实例,每new一个实例相当于创建一个新对象,每个实例之间是相互独立的。
构造函数:类实例是由一个特殊的类方法构造的,这个方法名同车类名相同,被称为构造函数,这个方法的任务就是初始化实例需要的所有信息。构造函数会返回一个对象(也就是类的一个实例)。类构造函数属于类,而且通常和类同名。此外,构造函数大多需要使用new来调(大多:构造函数本身也是函数,可以像普通函数一样被调用也可以使用new操作符以构造函数的方式被调用),这样语言引擎才知道你想要构造一个新的类实例。
new命令的原理
- 创建一个空对象,作为将要返回的对象实例
- 将这个空的对象原型对象,指向了构造函数的prototype属性对象
- 将这个实例对象的值赋值给函数内部的this关键字
- 执行构造函数体内的代码。在函数没有返回其他对象的情况下返回该新对象。如果构造函数没有返回值,则返回新创建的对象,如果有返回值:若返回值为基本类型时直接忽略返回新创建的对象,若返回值为引用类型则忽略新创建的对象返回该引用类型
类的继承
继承:定义好一个子类后,相对于父类来说它就是一个独立并且完全不同的类。子类会包含父类行为的原始副本,但是也可以重写所以继承的行为甚至定义新行为。注意,这里讨论的父类和子类都不是实例。
多态:子类继承父类的行为可重写,且不会影响到父类原有的同名行为。同时在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义(使用弗雷西指针指向子类创建的对象,调用方法时会产生不同的i行为)。
多重继承:js本身并不提供多重继承的功能
混入
在继承或实例化时,js对象机制并不会自动执行复制行为,简单来说js中只有对象并不存在可以被实例化的类,一个对象并不会被复制到其他对象,它们会被关联起来。由于在其他语言中类表现出来的都是复制行为,因此js开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。
显式混入
优势:可以把一个对象的属性复制到另一个对象中
缺点:函数引用问题
function mixin(sourceobj,targetobj){
for(var key in sourceobj){
if(!(key in targetobj)){
targetobj[key] = sourceobj[key];//从技术角度说,函数实际上没有被复制,复制的是函数引用
}
}
return targetobj
}
var Vehicle = {
engines:1,
ignition:function(){
console.log("Turn on my engine");
},
drive:function(){
console.log(this);
this.ignition();
console.log("Steering and moving forward");
}
};
var Car = mixin(Vehicle,{
wheels:4,
drive:function(){
Vehicle.drive.call(this);//this指向Car,Car重写的drive属性也重新加载了Vehicle中的drive
console.log("Rolling on all "+this.wheels+" wheels");
}
});
Car.drive();
// var bar = Car.drive;
// bar.call(Car);
显式伪多态会在所有需要伪多态引用的地方创建一个函数关联(显式伪多态只能就地调用父对象的同名函数,使得调用代码充斥在不同方法中,不如其他语言中extend关键字来的方便),这会极大的增加维护成本。除此之外,由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。
隐式混入
借用类1的函数A并且在类2的上下文中调用了它(通过this绑定),这叫把类1的行为混入了类2。
第九章 原型
[[prototype]]
对于默认的[[Get]]操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[prototype]]链
所有普通的[[prototype]]链最终都会指向内置的Object.prototype。
属性设置和屏蔽:
var anotherobj = {
a:2
}
var myobj = Object.create(anotherobj);
console.log(anotherobj.a);//2
console.log(myobj.a);//3
//hasOwnProperty是js中唯一一个处理属性但是不查找原型链的函数
console.log(anotherobj.hasOwnProperty("a"));//true
console.log(myobj.hasOwnProperty("a"));//false
myobj.a++;
console.log(anotherobj.a);//2
console.log(myobj.a);//3
console.log(myobj.hasOwnProperty("a"));//true
尽管myobj.a++看起来应该是通过委托查找并增加anotherobj.a属性,但是++操作相当于myobj.a=myobj.a+1。因此++操作首先会通过[[prototype]]查找属性a并从anotherobj.a获取当前属性+1,然后接着用[[Put]]将值3赋给myobj中新建的屏蔽属性a。
类
js中没有类没有类没有类,只有对象对象对象。但是js一直在模仿类。
js中函数特性:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它会指向另一个对象。当使用new调用函数变为构造函数时默认会将实例的[[prototype]]指向构造函数的prototype。
js不能创建一个类的多个实例,只能创建多个对象,它们[[prototype]]关联的是同一个对象。但是你在默认情况下并不会进行赋值,因此这些对象之间并不会完全失去联系,它们是相互关联的。在js中我们并不会将一个对象(类)赋值到另一个对象(实例),只是将他们关联起来。
继承意味着赋值,js并不会赋值对象的属性。相反js会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。委托更能准确的描述js中对象的关联机制。
构造函数
构造函数最准确的解释是:所有带new的函数调用。
并非函数本身就是一个构造函数,而是当你在普通函数调用前面加上new关键字,就会把这个函数调用编程一个构造函数调用。实际上,new会劫持所有普通函数并用构造函数的形式来调用它。
每个对象在创建时都会自动拥有一个constructor属性默认指向构造函数。实际上这个对象本身没有constructor属性,是往上原型链找到构造函数的prototype里找到了其constructor最终会指向构造函数本身。
//Person函数在声明时不仅产生了一个对象Person.prototype,而且这个对象默认存在一个属性时constructor指向这个Person函数本身
Person.prototype.constructor==Person;//true
如果你创建了一个新对象并替换了函数默认的.prototype对象引用,那么新对象并不会自动获得.constructor属性。但是这个Foo原型也没有.constructor属性,所以它会继续委托,这次会委托给委托链顶端的Object.prototype,这个对象有.condtructor属性,指向内置的Object函数。
由于函数默认的constructor并不是一个不可变属性,它是不可枚举但值是可修改的,你还可以给任意[[prototype]]链中的任意对象添加一个名为constructor的属性或者对其进行修改,你可以任意对其赋值。所以这样非常不可靠也不安全,如果默认的constructor属性一旦被修改,那么它所有实例如果有用到该属性,都会发生出乎意料的结果。
function Foo(){...};
Foo.prototype = {...};//创建一个新原型对象
//给新原型对象添加constructor属性
Object.defineProperty(Foo.prototype,"constructor",{
enumerable:false,
writable:true,
configurable:true,
value:...//修改constructor指向
});
var a1 = new Foo();
a1.construtor == Foo;//false
a1.construtor == Object;//true
使用构造函数创建对象的优缺点
优点:创建对象方便
缺点:占用空间,浪费系统资源(对于每一个实例对象render方法都是一模一样的内容,但是每生成一次实例,只要调用里面的方法,这个方法在内存中就会开辟一个新的空间,就会多用一些内存,如果有很多的实例,那么就会造成极大的内存浪费)。如果在全局情况下声明函数,虽然解决了内存资源浪费的问题,但是又会出现全局变量污染的问题。可以重新声明一个对象专门存放这些方法,但是新的问题是如果有很多个构造函数,就要声明很多个这样的对象。
console.log(p1.render === p2.render) // 此时为 false 表示不是同一个地址 。
解决:使用原型对象。对象中公共的成员可以放到原型对象中去,这样实例化出来的所有的对象都可以访问,对象中的成员的访问规则是如果对象中有,先访问自己的如果没有在从原型中找。
var Person1 = new Person('小白', 12)
Person1.sayhi = function(){
console.log('这是对象自己的方法');
}
Person.prototype.sayhi = function(){
console.log('这是添加在原型中的方法');
}
继承
Object.create(XXX) 和 new XXX()创建函数的区别
new XXX()
//new函数详细参阅构造函数
function Foo(){
this.b =2 ;
}
//new XXX()原理
// 1 创建一个空对象:var obj = new Object();
// 2 将这个新对象的__proto__指向构造函数的prototype:obj.__proto__ = F.prototype;
// 3 将构造函数的作用域赋值给新对象 (也就是this指向新对象)
// 4 执行构造函数中的代码(为这个新对象添加属性)
// 5 返回新的对象
var a = new Foo();
console.log(a.__proto__==Foo.prototype);//true
console.log(a.b);//2
Object.create(XXX)
function Foo(){
this.b =2 ;
}
// 1 创建一个空函数;
// 2 函数的prototype指向Base函数;
// 3 new一个函数的实例(即让该实例的__proto__指向函数F的prototype,也就是Base函数,最后将该实例返回)
// 使用Object.create()创建的对象是空的,该对象的.prototype 抛弃关联的默认对象,指向Object.create()的参数。
// Object.create = function (Base) {
// var F = function () {};
// F.prototype = Base;
// return new F();
// };
// Object.create() 创建的新对象的原型指向接收的参数本身,new Object() 创建的新对象的原型指向的是 Objec的prototype。
var a = Object.create(Foo);
console.log(a.__proto__ == Foo.prototype);//false
console.log(a.__proto__ == Foo);//true
//所以我们一般不这么创建而是下面这种方式
var a = Object.create(Foo.prototype);
console.log(a.__proto__ == Foo.prototype);//true
console.log(a.__proto__ == Foo);//false
var Base = function(){
this.a = 2;
};
var Base2 = {
a:1,
}
var base_1 = Object.create(Base);
//因为base_1是new F()返回的实例,这个F.prototype为Base,所以new F()本身不含a属性
console.log(base_1.__proto__ == Base);//true
console.log(base_1.a);//undefined
var base_2 = Object.create(Base2);
console.log(base_2.__proto__ == Base2);//true
console.log(base_2.a);//1
Object.create(null,{…})还可以创建一个拥有空[[prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof也无法进行判断,因此这种特殊的空对象通常被称为字典,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
比较 | new | Object.create |
继承 | 保留原构造函数的属性 | 参数是函数的话会丢失这个函数的属性,参数是对象的话会保留对象的属性 |
_proto_ | 指向原构造函数prototype属性 | 根据参数不同指向原构造函数本身或对象本身 |
作用对象 | function(new的是函数) | function和object(参数是函数或对象) |
错误做法
一、Bar.prototype = Foo.prototype
这样并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype。当你执行类似Bar.prototype.mayLabel的赋值语句会直接修改Foo.prototype对象本身。
二、Bar.prototype = new Foo();
这样确实会创建一个关联到Foo.prototype的新对象,但是它使用了构造函数调用,如果函数Foo的一些副作用(比如自定义行为)就会影响到Bar后代。即将来把Bar生成的对象中某一来自Foo的属性删除,但你会发现其并不等于undefined,访问时仍有值,这个值旧来自原型链上。
正确做法
在ES6之前这种方法Object.create(…)唯一的缺点是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。
ES6添加了辅助函数Object.setPropertyOf(…)可以用标准并且可靠的方式修改关联。
BarProp.prototype = Object.create(Foo.prototype)
Object.setPrototypeOf(Bar.prototype,Foo.prototype)
寻找原型链
检查一个js对象的继承祖先(js中的委托关联)通常被称为内省或反射。
【一个对象 instanceof 一个函数】,其回答的问题时在对象的整条原型链中是否有函数原型指向的对象。
//硬绑定生成函数会没有.prototyp属性
var Foo1 = Foo.bind();
var a = new Foo1();
console.log(a instanceof Foo1);//true
console.log(a instanceof Foo);//true
console.log(Foo1.prototype);//undefined
【两个对象】:isPrototypeOf(b是否在c的原型链中)
直接获取一个对象的原型链:
- Object.getPrototypeOf(a);
- a._proto_
对象关联
原型链机制的意义是什么呢?我们不需要类来此创建两个对象之间的关系,只需要通过委托来关联对象旧足够了。而且Object.create(XXX) 不包含任何类的诡计,所以它可以完美的创建我们想要的关联关系。其还可以有第二个参数,制定了需要添加进新对象中的属性名以及这些属性的属性描述符。
图源:帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
关联关系是备用
看起来对象之间的关联关系是处理“缺失“属性或方法时的一种备用选项,但这并不是[[prototype]]本质,
内部委托比起直接委托可以让API接口更清晰。对外暴露的API应该都是在我对象中声明过的,不管是它默认的行为还是通过原型链继承来的属性或方法。
var anotherobj ={
cool:function(){
console.log("cool");
}
};
var myobj = Object.create(anotherobj);
console.log(myobj);
//通过[[prototype]]委托到anotherobj.cool()
myobj.docool = function(){
console.log(this);//docool
this.cool();
};
myobj.docool();
第十章 行为委托
本章内容对于如何更好理解类和对象关联这两种设计模式有很大的帮助。同时也教授了很多代码编写的技巧和各种代码风格的优缺点。同时对ES6中的class和鸭子模型等进行了简要了介绍,值得反复观看和理解。