001 for循环中定义循环变量,不要使用var,尽量用let
function fn1() {
var arr = []
for (var i=0;i<3;i++) {
arr.push(function () {
return i
})
}
return arr
}
var a = fn1()
console.log(a[1]()) //3 arr=[3,3,3]
function fn1() {
var arr = []
for (var i=0;i<3;i++) {
arr.push(function () {
return i
}())
}
return arr
}
var a = fn1()
console.log(a[1]) //1 arr=[0,1,2]
function fn1() {
var arr = []
for (let i=0;i<3;i++) {
arr.push(function () {
return i
})
}
return arr
}
var a = fn1()
console.log(a[1]()) //1 arr=[0,1,2]
这是一个不容易发现的巨坑,原因是for循环的()中用var定义的变量并不在for循环{}内,因此是for循环外面的变量,这必然会污染外层变量。for循环中的语句一旦涉及到循环变量,一定会出错。
002 函数定义了,那么怎么让它执行呢?(怎样让函数运行)
function fn2() {
var arr=[]
for(var i=0;i<3;i++){
arr.push((function (num){
return function () {
return num
}
})(i))
}
return arr
}
var b = fn2()
console.log(b[0]()) //0 arr=[0,1,2]
function fn2() {
var arr=[]
for(var i=0;i<3;i++){
arr.push((function (num){
function dd() {
return num
}
return dd
})(i))
}
return arr
}
var b = fn2()
console.log(b[0]()) //0 arr=[0,1,2]
function fn2() {
var arr=[]
for(var i=0;i<3;i++){
arr.push((function (num){
function dd() {
return num
}
return dd()
})(i))
}
return arr
}
var b = fn2()
console.log(b[0]) //0 arr=[0,1,2]
为了让函数fn2()内部的匿名函数执行,在b[0]后加(),即b[0]();然后我让匿名函数具名为dd(),但是我让它返回dd,而不是返回函数dd的结构dd(),所以依然需要b[0]();再然后我返回dd(),这样就不需要b[0]了。
003 你把函数赋值给了一个变量,然后该变量怎样调用了一个函数?鬼知道会出现什么样的格式?
function fn1() {
var arr = []
for (var i=0;i<3;i++) {
arr.push(function () {
return i
})
}
return arr
}
var a = fn1()
console.log(fn1()) //(3) [ƒ, ƒ, ƒ]
function fn1() {
var arr = []
for (var i=0;i<3;i++) {
arr.push(function () {
return i
})
}
return arr
}
var a = fn1()
console.log(a) //(3) [ƒ, ƒ, ƒ]
上面两段代码没有执行push里面的匿名函数,真正push进arr数组里面的是匿名函数。
function fn1() {
var arr = []
for (var i=0;i<3;i++) {
arr.push(function () {
return i
})
}
return arr
}
console.log(fn1()[2]()) //3 arr=[3,3,3]
这一段代码真正的让push里面的匿名函数执行了,但是形式太奇怪。fn1()加[2]是为了指定数组元素,加()则是为了匿名函数一定执行,你指定数组中其它元素也行,但是匿名函数一定要让它执行。
004 this指针真是个迷
var color = "red";
var obj = {
color:"yello",
fn:function(){
return function (){
return this.color;
}
}
}
var b = obj.fn()
console.log(b()) //red
为什么会这样呢?b是一个全局变量,this指针有一个特点,谁调用,指向谁,所以指向了window下的color。
var color = "red";
function fn(){
return this.color;
}
var obj = {
color:"yello",
fn:function(){
return function (){
return this.color;
}
}
}
var b = obj.fn()
console.log(b.call(obj)) //yello
console.log(b.apply(obj))//yello
console.log(fn.call(obj))//yello
使用call()、apply()函数指定函数的this指针。
var color = "red"
function fn(){
return this.color
}
var obj = {
color:"yellow",
fn:function(){
var that=this
return function(){
return that.color
}
}
}
console.log(fn())//red
var b = obj.fn()
console.log(b())//yello
用that做一下中转也行
005 get与set方法,用JavaScript函数模仿java的类。
function Desk(){
var str = ""
this.getStr = function(){
return str
}
this.setStr = function(value){
str = value
}
}
var desk = new Desk()
desk.setStr("lihong")
console.log(desk.getStr())//lihong
006 JavaScript中for循环和if条件对var变量不形成作用域
function fn(num){
for(var i=0;i<num;i++){}
console.log(i) //2
}
fn(2)
if(true){
var a = 13;
}
console.log(a) //13
用函数来形成局部作用域
(function(){
for(var i = 0; i < 9; i++){}
})();
console.log(i) // i is not defined
007 这个不算是坑,是一个应用实例。JavaScript对象和JSON的转换例子
var str = '{"name":"huangxiaojian","age":"23"}';
json1 = JSON.parse(str);
console.log(json1)
var student = new Object()
student.age = "18"
student.name = "lili"
json2 = JSON.stringify(student)
alert(json2)
008 局部变量与全局变量
var a=1;
var b=2;
(function(){
var b=3;
a+=b
})();
console.log(a);//4
console.log(b);//2
009 将匿名函数赋值给一个变量,然后通过变量加()来调用,很简单,一看就懂。
var demo=function(){
console.log("helloworld")
}
demo()
010 不要使用一个未定义的变量,所有未定义而被使用的变量,都会被默认定义为全局变量,污染全局变量。
var total=1
var demo=function(){
total=10
return total
}
console.log(total)//1
console.log(demo())//10
console.log(total)//10
匿名函数里面使用了未定义的变量total,如果匿名函数没有执行,那么total就不会泄露到全局,一旦demo()执行,total就覆盖了全局的total。用var或者let定义一下都行。
var total=1
var demo=function(){
var total=10
return total
}
console.log(total)//1
console.log(demo())//10
console.log(total)//1
011 这个不算坑,一个JavaScript的应用实例。匿名自执行函数,可以带参,也可以不带参。
(function(){
console.log("匿名自执行函数")
})();
(function(x,y){
console.log(x+y)
})(1,2); //3
(function(m,n){
var total=0
if(m >= n){
console.log("求值")
return false
}
for(var i=m;i<=n;i++){
total +=i;
}
console.log(total)
})(1,100); //5050
012 JavaScript的提升机制,导致了变量可以先使用,后定义,函数可以先使用,后定义,但是现在来看,就是一个鸡肋。
function test_1(){
console.log(a) //undefined
var a=123
}
test_1()
function test_2(){
var a=123
console.log(a) //123
}
test_2()
a=1;
var a; //提升到了a=1之上
console.log(a) //1
console.log(v1) //undefined
var v1=100
function foo(){
console.log(v1)
var v1=200
console.log(v1)
}
foo() //undefined 200
console.log(v1) //100
console.log(v1) //undefined
var v1=100
foo() //undefined 200
function foo(){
console.log(v1)
var v1=200
console.log(v1)
}
console.log(v1) //100
需要明白的是类似var a=1这样的语句会被拆分成var a; a=1;两句话,其中var a会被提升。
013 匿名函数赋值给变量,变量调用匿名函数,赋值语句不会提升,所以通过变量调用匿名函数发生在赋值之前,要不报error要不报undefined。重点就是赋值语句不提升。这大概就是直接通过函数名调用函数和通过变量间接调用函数的差别。
console.log(foo_2()) //error foo_2 is not defined
foo_2 = function (){
console.log(v1)
var v1=200
console.log(v1)
}
console.log(foo_2()) //undefined 200 undefined
console.log(foo_2) //error foo_2 is not defined
foo_2 = function (){
console.log(v1)
var v1=200
console.log(v1)
}
console.log(foo_2()) //undefined 200 undefined
console.log(foo_2()) //error foo_2 is not a function
var foo_2//手动添加声明
foo_2 = function (){
console.log(v1)
var v1=200
console.log(v1)
}
console.log(foo_2()) //undefined 200 undefined
console.log(foo_2) //undefined
var foo_2//手动添加声明
foo_2 = function (){
console.log(v1)
var v1=200
console.log(v1)
}
console.log(foo_2()) //undefined 200 undefined
014 JavaScript的变量提升与函数提升优先级,函数创建有两种方式:1、函数申明形式;2、函数字面量形式(即函数表达式)。【而只有函数声明形式才有函数提升】,还有一种是方式:函数构造法:var a = new Fun(),技术角度来讲也是一个字面量形式。
// 声明式
function foo () {
// to do...
}
函数提升,相当于:
// 函数字面量
var foo = function () {
// to do...
}
函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖。
console.log(bar); // f bar() { console.log(123) }
console.log(bar()); // undefined
var bar = 456;
function bar() {
console.log(123); // 123
}
console.log(bar); // 456
bar = 789;
console.log(bar); // 789
console.log(bar()) // bar is not a function
相当于:
// js执行步骤
// 函数提升,函数提升优先级高于变量提升
var bar = function() {
console.log(123) // 123
};
// 变量提升,变量提升不会覆盖(同名)函数提升,只有变量再次赋值时,才会被覆盖
var bar;
console.log(bar); // f bar() { console.log(123) }
console.log(bar()); // undefined
// 变量赋值,覆盖同名函数字面量
bar = 456;
console.log(bar); // 456
// 再次赋值
bar = 789
console.log(bar); // 789
console.log(bar()); // bar is not a function
其实这样生硬地解释函数申明式和字面量式之间的区别还是欠妥的,合理地说,函数声明式会转化为函数声明和函数赋值两部分,声明部分会提升,赋值部分会留在原地。
function foo(){
console.log(1);
}
变成
var foo
foo = function(){
console.log(1);
}
这个时候,你调用foo()函数的调用语句的位置就很关键了,到底是介于声明和赋值的什么位置,会影响结果;在声明之后赋值之前,会输出undefined或者error(foo is not a function),在声明之后赋值之后,会输出正确结果。
015 简单例子,看看就好。
什么叫函数提升啊,小例子。
foo();// 1
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
用匿名自执行函数
var a = true;
if(a){
(function foo(){
console.log(1);
})()
}
else{
(function foo(){
console.log(2);
})()
}
函数有返回值和无返回值的区别
function f_2(){
console.log("helloworld");
}
console.log(f_2());//helloworld
function f_3(){
console.log("helloworld");
return 1;
}
console.log(f_3());//helloworld undefined
变量提升的一个小例子,这里能看懂。
var x=5
function f(){
console.log(x)
var x = 2;
}
f() //undefined
赋值覆盖的一个小例子,这里是变量的赋值覆盖,后面还可能碰到变量赋值覆盖了同名函数的例子
var x=5
var x=10
console.log(x) //10
赋值覆盖的另一个例子,变量的赋值覆盖了同名的另一个函数,导致输出not a function
var f_1
function f_1(){
console.log("helloworld")
}
f_1() //helloworld
var f_2 =5
function f_2(){
console.log("helloworld")
}
f_2() //error f_2 is not a function
变量提升、函数提升、变量赋值覆盖函数的一个综合的例子,看懂的话,就差不多理解这里的知识点了。
console.log(x); // f
var x=2;
console.log(x); // 2
function x(){
console.log(3)
}
console.log(x); // 2
var x=3;
console.log(x); // 3
function x(){
console.log(5)
}
console.log(x); //3
x(); //error x is not a function
局部变量覆盖全局变量
var a=10;
(function(){
var a=20;
console.log(a); //20
})();
var a=10;
(function(){
console.log(a); //10
})();
016 字面量式函数声明,说它没有提升是不完全正确的,还是要理解成声明和赋值两部分,声明部分提升,赋值部分原地;所以在声明之后赋值之前,有f无f(),赋值使得f()成为一个真正的函数。
console.log(f) //undefined
var f = function(){
console.log("hi")
return 1
}
console.log(f) // f
console.log(f()) // hi 1
console.log(f()) //error f is not a function
var f = function(){
console.log("hi")
return 1
}
console.log(f) // f
console.log(f()) // hi 1
这一个例子是函数申明式和函数字面量式混合的形式,是一种不规范的写法,可以看到在函数声明之后赋值之后,f是function类型,而foo是undefined类型,但是在函数执行的内部,依然是可以侦测到foo是function类型的,说明什么问题呢?这种写法使foo在外部失效了,只有当函数执行的时候,在函数体内部依稀能够侦测到foo是一个function,也就是说foo被降级成一个匿名函数了。匿名函数只有在运行时,系统才会为它开辟内存。
var f = function foo(){
console.log(1)
};
f() //1
foo() //error foo is not defined
var f = function foo(){
console.log(typeof foo);
};
f() //function
foo() //error foo is not defined
var f = function foo(){
console.log(typeof foo);
};
console.log(typeof foo); //undefined
f(); //function
console.log(typeof f); //function
017 函数名与变量名同名,变量赋值覆盖函数名。
function value_1(){
return 1;
}
var value_1;
console.log(typeof value_1) //function
function value_2(){
return 1;
}
var value_2 = 1;
console.log(typeof value_2) //number
局部无此变量,那就到外面一层作用域找吧。
var a = 2;
function test(){
console.log(a);
}
test(); //2
018 有关setTimeout异步函数,巨坑。
for(var i=1; i<=5;i++) {
setTimeout(function(){
console.log(i);
}, i*1000)
}
//6 6 6 6 6
这是因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,
但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,
才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行function匿名函数,
但是当for循环结束后此时i的值已经变成了6,因此虽然定时器跑了5秒,控制台上的内容依然是6。
for(var i=1; i<=5;i++) {
(function(i){
setTimeout(function(){
console.log(i);
}, i*1000)
})(i)
}
//6 6 6 6 6
不放到队列里面去等待执行,而是借用带参匿名自执行函数让它立刻就执行掉
for(let i=1; i<=5;i++) {
setTimeout(function(){
console.log(i);
}, i*1000)
}
//1 2 3 4 5
let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。
setTimeout里面的function()属于一个新的域,通过var定义的变量是无法传入到这个函数执行域中的,
通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;
这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。
这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。
019 超级变态的坑,变量名和函数名重名,且变量名和函数名有提升。test_1()函数内的赋值语句foo_1 = 5,foo_1未使用var或者let修饰,使其污染全局变量。foo_2 = 5;也未使用var或者let修饰,似乎也会污染全局变量,但是foo_2()函数提升的优先级比它高,所以变量foo_2污染的是函数test_2()内部的函数名foo_2,使其从function变成了number。
var foo_1 = 2;
test_1(); //1
function test_1(){
foo_1 = 5;
(function foo_1(){
console.log(1);
})();
}
console.log(foo_1); //5
var foo_2 = 2;
test_2();
function test_2(){
foo_2 = 5;
function foo_2(){
console.log(1);
}
}
console.log(foo_2); //2
020 JavaScript的this指针,巨坑中的巨坑,特别是涉及到匿名函数的this指针。匿名函数最经典的总结:就是谁调用匿名函数,匿名函数中的this就指向谁;匿名函数是有执行上下文,只是执行上下文是执行的时候传递过来,箭头函数中的执行上下文是父级的执行上下文,匿名函数可以作为箭头函数的父级。匿名函数又要和立即执行函数区分开,立即执行函数的this指针都指向window,这一点不用怀疑。
var obj_1 = {
val:1,
outshow : function(){
(function(){
console.log(this);//window
})();
console.log(this)//obj_1
}
};
obj_1.outshow();
匿名函数里面还有一个立即执行函数,该立即执行函数的this执行window,匿名函数中console.log(this)指向的是调用者obj_1。
var obj_2 = {
val:1,
outshow : function(){
var that = this;
(function(){
console.log(that);
})();
}
}
obj_2.outshow();// obj_2
匿名函数里面的this执行它的上下文obj_2,通过that中转,使立即执行函数执行obj_2而不是window。
var obj_3 = {
val:1,
outshow : function(){
var innerFunc = function(){
console.log(this.val);
};
innerFunc.bind(this)();
innerFunc.call(this);
innerFunc.apply(this);
}
}
obj_3.outshow();
强制手法
下面再看几个例子,彻底明白匿名函数、箭头函数、自执行函数的this指向问题。
//谁调用匿名函数,匿名函数中的this就指向谁;匿名函数是有执行上下文,
//只是执行上下文是执行的时候传递过来
var name='window';
var obj = {
name:'obj',
nameprintf:function(){
console.log(this.name)
}
}
var obj1 = {
name:'obj1'
}
obj.nameprintf();//'obj'
var w = obj.nameprintf;
w();//'window',这时候相当于Window.w(),this自然指向的是Window
obj.nameprintf.call(obj1) //obj1,匿名函数的中的this已经被绑定到了obj1
//箭头函数它会直接绑定到它父级的执行上下文里的this
var name='window';
var obj = {
name:'obj',
nameprintf:()=>{
console.log(this.name)
}
}
obj.nameprintf();//window,因为此时父级是obj,obj的上下文是window
var obj={
num:4,
fn:function(){
console.log(this); //匿名函数的this,应该是谁调用指向谁
(() => {
console.log(this);//箭头函数的this,应该是父级的执行上下文,这里父级是fn,
//上下文就是调用fn函数的那个对象,和上面的this指向是一样的
})();
}
}
obj.fn(); //指向的都是obj
var w = obj.fn;
w(); //指向的Window
var obj1 = {name:'obj1', fn:obj.fn}
obj1.fn(); //指向的obj1
obj.fn.call(obj1); //也是指向obj1
var obj = {
val:1,
show:function(){
console.log(this.val);
}
}
obj.show(); //1
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
function innerFunc(){
console.log(this.val);
console.log(this);
}
innerFunc();
console.log(this)
}
};
obj.show(); //1
obj.outFunc(); //undefined window obj
内层函数定义以后,让它立刻执行,效果就跟自执行函数差不多,this指针指向的依然是window(自执行函数的this指针指向window)。
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
function innerFunc(){
console.log(this);
function f() {
console.log(this)
}
f()
}
innerFunc();
console.log(this)
}
};
obj.show(); //1
obj.outFunc(); //undefined window window obj
匿名函数的this指针是谁调用指向谁,所以outFunc里面的this指向了obj,但是innerFunc是有名函数,它的this竟然指向了window,奇了怪了,感觉innerFunc就像立即执行函数一样。
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
function innerFunc(){
console.log(this.val);
console.log(this);
}
}
};
obj.show(); //1
obj.outFunc()(); //error: obj.outFunc(...) is not a function
021 箭头函数实例
var fn_1 = (a,b) => {
var num = a+b;
console.log(num);
}
fn_1(1,2);//3
var fn_2 = (a,b) => {
console.log(this);
}
fn_2();//window
箭头函数没有this指针,它的this指针是继承上下文环境的,也就是fn_2的this指针,指向window。
022 JavaScript对象,new一个新的对象。有多种创建对象的方式,比如字面式创建(最容易理解)、工程模式、构造函数模式,后两种需要借用关键字function,这最容易让人误解了。其实工程模式就是用函数封装一个字面式创建过程,在工程模式内部,通过字面式创建了一个Object,然后给Object添加属性和方法。
function creatPerson(name,age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function () {
console.log(this.name);
}
return o;
}
var o1 = creatPerson('zhang',12);
var o2 = creatPerson('js',12);
o1.sayName()//zhang
o2.sayName()//js
console.log(o1.sayName === o2.sayName); //false
console.log(o1 instanceof Object);// true
而构造函数模式也是通过function关键字,但是呢构造函数模式并不用一个函数去包裹一个字面式表达式创建过程。关键点是通过new关键字去调用function构造函数,所以构造函数的类型依然是function,但是对象实例的类型是object,我们甚至都看不到C++或者java中的那样的一个类的创建过程。只丢了一个函数给你。
function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;
this.sing = function(sang) {
console.log(sang);
console.log(this)
}
}
console.log(typeof Star)//function
var ldh = new Star('刘德华', 18, '男'); // 调用函数返回的是一个对象
console.log(typeof ldh);//object
console.log(ldh.name);//刘德华
console.log(ldh['sex']);//男
console.log(ldh['age'])//18
ldh.sing('冰雨');//冰雨 Star
var zxy = new Star('张学友', 19, '男');// 调用函数返回的是一个对象
console.log(typeof zxy);//object
console.log(zxy.name);//张学友
console.log(zxy.age);//19
console.log(zxy.sex)//男
zxy.sing('李香兰')//李兰香 Star
上面还是使用一个变量var ldh = new Star(),然后通过ldh来调用对象的属性或者方法。下面还有写得更简洁的,创建到调用一步执行,符合项目代码风格。
new Star('刘德华', 18, '男').sing('你好世界')//你好世界 Star
怎么解释new一个函数,就创建出一个对象这种行为呢?如果你用函数名()的方式,那就是调用函数,如果你用new 函数名()的方式,那就是创建一个对象。JavaScript里面的函数是最高级的对象,由于JavaScript的设计理念和传统的OOP有区别,所以很容易混淆。
function Person(){
console.log(this);
}
Person()//window
new Person()//Persion
上面是最简单的一个函数,通过函数的方式调用,this指向了调用者window,通过new 函数名()的方式调用,则生成了一个Persion对象,然后this指针就指向了这个新生成的对象。下面这个就比较讲究了,用了自执行函数来包裹一个构造函数,因为非匿名函数在定义时,就已经创建函数对象和作用域对象;所以,即使未调用,也占用内存空间;匿名函数,仅在调用时,才临时创建函数对象和作用域链对象;调用完,立即释放,所以匿名函数比非匿名函数更节省内存空间
(function(){
var name = '';
Person = function(value){
console.log(this);
}
})()
var p=new Person();//Persion
Person()//Window
023 来来来,看一个比较典型的例子,一下子理解匿名函数、箭头函数、立即执行函数、setTimeout函数以及对象之间的区别。
function foo(){
(() => {
console.log(this);
})();
(function () {
console.log(this)
})();
console.log(this);
setTimeout(function(){
console.log(this)
},1000);
setTimeout( () => {
console.log(this)
},2000)
}
foo()//window window window window window
new foo()//foo{} window foo{} window foo{}
通过函数方式foo()调用函数,结果都指向了window;通过new 函数()生成对象的方式,让我们可以看到匿名函数与箭头函数的区别。箭头函数的this指针是继承父级上下文的,一个立即执行函数包裹了它,立即执行函数处于构造函数内部,实际上父级上下文就是foo,所以指向了foo;匿名函数的this指针是指向调用者的,但是这个调用者往往很难确定,并不建议大家使用链太复杂的匿名函数。需要区别的是立即执行函数内部并不一定只装匿名函数,有时候立即执行函数内部也可以装箭头函数;setTimeout函数是一个延时执行函数,它会把放置在它内部的函数先放置到任务队列中,等主任务执行完以后,再把任务队列里面的函数拿出来执行。有时候往setTimeout里面塞匿名函数,有时候往setTimeout里面塞箭头函数。谁调用匿名函数,匿名函数中的this就指向谁;匿名函数是有执行上下文,只是执行上下文是执行的时候传递过来,箭头函数中的执行上下文是父级的执行上下文,匿名函数可以作为箭头函数的父级。
024 无关匿名函数与箭头函数的区别,主要讲回调函数的this指针问题,填一个坑。不只是匿名函数与箭头函数之间存在this指针的区别,普通函数也有可能掉进this指针的坑。下面就是普通函数回调,掉进了this指针的坑。
var o_1 = {
age:12,
say:function(){
function callback(){
return this.age;
}
func_1(callback);
console.log(this)
}
};
function func_1(callback){
var name = "xiaoming";
console.log(name + " is " + callback() + " years old.");
console.log(this)
}
o_1.say();//xiaoming is undefined years old.
//window
//o_1
可以看到o_1调用say的属性方法,即匿名函数调用func_1,func_1是全局的一个函数,func_1又调用callback()方法,callback()函数里面返回this.age。按照C++或者C语言里面的思维,最后的this应该指向最开始的调用者。但是这种调用关系很复杂,不能想当然的这样理解,其实函数调用是往栈区存放函数的地址,一个一个的往栈区存放,如果调用关系简单,不会出错,一旦嵌套太多,很难保证不出错。然后this就找不到鸟^_^。可以看到在执行fun_1函数之前,匿名函数里面的this指针没有出错,因为照样找到了o_1。在进入func_1(),再进入callback()之后,this指针就乱了。这种函数的执行上下文真的是个迷啊,原型链的知识掌握得不够。下面再看箭头函数的表现。如果把函数调用考虑到压栈那个级别,那么在进入func_1这里的时候,就重新开了一个栈,func_1作为一个新栈的第一个被压栈元素,跟前一个栈已经断开了。也就是函数调用链断开了。有可能,一旦栈重返window就断了吧。建议回调函数统统用箭头函数写,这样就不会因为外部函数的调用导致this指针乱了。
var o_3 = {
age:12,
say:function(){
var callback = () =>this.age;
func_3(callback);
console.log(this)
}
};
function func_3(callback){
var name = "xiaoming";
console.log(name + " is " + callback() + " years old.");
console.log(this)
}
o_3.say();//xiaoming is 12 years old.
//window
//o_3
可以看到o_3调用say的属性方法,即匿名函数调用func_3,func_3是全局的一个函数,func_3又调用callback()方法,callback()函数里面返回this.age,但是callback()是一个无参箭头函数,它没有this指针,它的this指针是继承父级上下文的,即say属性方法对应的那个匿名函数的,这个匿名函数的调用者是o_3,所以最后也就指向了o_3。o_1例子中确实是在调用外部的func_1函数的时候导致了this指针的丢失,即调用链的中断。那么怎样把断掉的调用链连起来呢?用that作为参数,在调用func_4函数的时候,将say属性方法那个匿名函数的上下文执行环境this传给func_4,在回调的时候,再把func_4的this回调给callback。
var o_4 = {
age:12,
say:function(){
function callback(that){
return that.age;
}
func_4(this,callback);
console.log(this)
}
};
function func_4(that,callback){
var name = "xiaoming";
console.log(name + " is " + callback(that) + " years old.");
console.log(this)
}
o_4.say();//xiaoming is 12 years old.
//window
//o_4
为了验证上面的想法,我们来看下面的例子。这个例子是函数嵌套调用的例子,使用的依然是箭头函数,这样便没有出现this指针丢失的情况。当然了,也可以把箭头函数改成普通函数,但是那样就需要显示地传递this指针了。这就是显示传递this指针和隐式传递this指针的区别了。
var o_5 = {
age:12,
say:function(){
var callback = () => this.age;
func_5(callback);
}
};
function func_5(callback){
f(callback);
}
function f(callback){
var name = "xiaoming";
console.log(name + " is " + callback() + " years old.");
}
o_5.say();//xiaoming is 12 years old.
var o_2 = {
age:12,
say:function(){
function callback(){
return o_2.age;
}
func_2(callback);
console.log(this)
}
};
function func_2(callback){
var name = "xiaoming";
console.log(name + " is " + callback() + " years old.");
console.log(this)
}
o_2.say();//xiaoming is 12 years old.
//window
//o_2
直接指定大法好,不会出错,只是没有那么优雅。
025 JavaScript的深拷贝与浅拷贝,我们知道JavaScript中一般有按值传递和按引用传递两种复制方式:按值传递的是基本数据类型,如Number,String,Boolean,Null,Undefined,Symbol,一般存放于内存中的栈区,存取速度快,存放量小。按引用传递的是引用类型,如Object,Array,Function,一般存放于内存中的堆区,存取速度慢,存放量大,其引用指针存于栈区,并指向引用本身。我们经常说的深浅拷贝是针对引用类型来说的:浅拷贝,指两个js对象指向同一个内存地址,其中一个改变会影响另一个;深拷贝,指复制后的新对象重新指向一个新的内存地址,两个对象改变互不影响。深浅拷贝的区别在于是否是真正获取了一个对象的复制实体,而不是引用,深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。下面看一个浅拷贝的例子。
var o1 = {
a : 6,
b : 6
};
var o2 = o1;
o2.a = 2;
o2.b = 2
console.log(o1.a);//2
console.log(o1.b);//2
console.log(o2.a);//2
console.log(o2.b);//2
改变对象o2就改变了对象o1,因为o2并没有在堆区获得一份独立的实体,o2仅仅是在栈区获得一个指针,指向了o1实体,所以o1与o2共用同一份堆区的对象实体。下面再看深拷贝的例子。通过构造函数的方式创建对象,很好的避免了浅拷贝的问题。
function f() {
var o1 = {
a : 6,
b : 6
};
return o1
}
var o2 = new f();
o2.a = 2;
o2.b = 2
console.log(f().a);//6
console.log(f().b);//6
console.log(o2.a);//2
console.log(o2.b);//2
console.log(o2)//{a:2,b:2}
JavaScript本身也提供一定的深拷贝函数,Object.assign()方法是ES6的新函数,只能简单的复制一层属性到目标对象,还得考虑兼容性。负责的深拷贝就要使用其它函数了JSON.parse(JSON.stringify(obj))是最简单粗暴的深拷贝,能够处理JSON格式的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝,而且会直接丢失相应的值,还有就是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。
var obj = { a: {c: "js"}, b: 22 };
var newObj = Object.assign({}, obj);
newObj.b = 22222;
newObj.a.c = 'javascript';
console.log(obj); // { a: { c: 'javascript' }, b: 22 }
console.log(newObj); // { a: { c: 'javascript' }, b: 22222 }
console.log(obj==newObj); // false
console.log(obj===newObj); // false
可以看到,这是不完全的深拷贝,b是深拷贝,c是浅拷贝。
var obj = { a: {c: "js"}, b: 33 };
var newObj = JSON.parse(JSON.stringify(obj));
newObj.b = 33333;
newObj.a.c = "hello js";
console.log(obj); // { a: { c: 'js' }, b: 33 }
console.log(newObj); // { a: { c: 'hello js' }, b: 33333 }
console.log(obj==newObj); // false
console.log(obj===newObj); // false
可以看到JSON.parse(JSON.stringify(obj))实现了彻底的深拷贝,b和c都是深拷贝。我们再试着去看看自己写的深拷贝构造函数,它也能够实现彻底的深拷贝。
function f() {
var o1 = {
a : 6,
b : {
c:4
}
};
return o1
}
var o2 = new f();
o2.a = 666;
o2.b.c = 444
console.log(f());//{a:6,b:{c:4}}
console.log(o2)//{a:666,b:{c:444}}
026 eval()是一个函数,有且只有一个参数string,为字符串类型,若string为js代码时,会直接解析执行,若是普通字符串,则返回原字符串。
console.log(eval('{foo:123}'))//123
console.log(eval( {foo:123} ))//{foo:123}
eval("var a =1;var b=4; alert(a+b)");//5
027 JavaScript拆箱技术。
var o = {
p:'hello world',
q: (function (){
console.log('q');
})
};
console.log(o.p);//helloworld
console.log(o['p']);//helloworld
console.log(Object.keys(o));//["p","q"]
var o = {
p:'hello world',
q: (function (){
console.log('q');
}),
a:1,
b:2,
c:3
};
for(var i in o){
console.log(i);// p q a b c
028 JavaScript的this指针补充
var obj = {
getAge : function(){
console.log(this)
var fn = function() {
console.log(this);
};
fn();
}
}
obj.getAge();// obj window
var obj = {
getAge: function () {
console.log(this);
var fn = () => console.log(this);
fn();
}
};
obj.getAge();// obj obj
029 JavaScript变量提升与函数提升补充
var birth =222;
var obj = {
birth: 1990,
getAge: function () {
var birth = 22;
console.log(this);//obj
console.log(this.birth);//1900
console.log(birth);//22
(function(){
console.log(this);//window
})();
(function(){
console.log(this.birth);//222
})();
}
};
obj.getAge();
test();//window
var foo = 2;
function test(){
foo = 5;
(function (){
console.log(this);
})();
}
console.log(foo);//2
var foo = 2;
test();//window
function test(){
foo = 5;
(function (){
console.log(this);
})();
}
console.log(foo);//5
030 嵌套的箭头函数
let makeAdder =(x =>{
return (y=>{
return x+y;
})
})
var add5 = makeAdder(5);
console.log(add5(2));//7
var add6 = makeAdder(10)(10);
console.log(add6);//20
var a = 1;
var aaa = function(x){
(()=>{
(()=>{
a++;
console.log(a);
})();
})();
};
aaa() //2
aaa() //3
031 数组元素也可以引用函数
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[9](); // 9
032 JavaScript对象浅拷贝
let dog = {type: 'animal', many: 2}
let { type, many} = dog
console.log(type, many) //animal 2
console.log(dog)//{type: "animal", many: 2}
033 JavaScript的map函数调用回调函数,形式上和setTimeout函数很像,将一个回调函数作为参数送入map函数。
function square(arr){
return arr.map(function(item){
return item*item;});
}
var arr=[1, 2, 3, 4];
console.log(square(arr));//[1,4,9,16]
034 JavaScript的一个函数和对象的例子,不知道怎么归类。
var aaa = (function(){
var a = 1;
function bbb(){
a++;
console.log(a);
}
function ccc(){
a++;
console.log(a);
}
return {
b:bbb,
c:ccc
}
})();
aaa.b(); //2
aaa.c(); //3
console.log(aaa);//{b:f,c:f}
因为匿名函数的返回值是一个对象,且这个匿名是立即执行函数,所以aaa得到的可是一个对象啊^_^。再看下面一个因为匿名函数没有自执行,所以aaa得到只是一个函数,得自己执行一遍,才能得到返回值(一个对象)。
var aaa = function(){
var a = 1;
function bbb(){
a++;
console.log(a);
}
function ccc(){
a++;
console.log(a);
}
return {
b:bbb,
c:ccc
}
};
aaa().b(); //2
aaa().c(); //3
console.log(aaa());//{b:f,c:f}
035 一个嵌套使用return的函数,我自己不会这么用的。
function box(){
var age = 100;
return function(){
age++;
return age;
};
}
var b = box();
console.log(b());//101
console.log(b());//102
console.log(b());//103
036 JavaScript的map函数内部的回调函数最好使用箭头函数,其它函数很容易发生this指针丢失。
window.name = '123';
const xyz1 = {
name: 'xyz',
hobbies: ['Coding', 'Sleeping', 'Reading'],
printHobbies:function(){
this.hobbies.map(function(hobby){
console.log(`${this.name} love ${hobby}`);
})
}
};
xyz1.printHobbies();//123 love Coding
//123 love Sleeping
//123 love Reading
匿名函数作为回调函数发生了this指针的丢失,原本想让this指针指向xyz,可是this指针指向了123。再来看箭头函数作为回调函数。
const xyz2 = {
name: 'xyz',
hobbies: ['Coding', 'Sleeping', 'Reading'],
printHobbies:function(){
this.hobbies.map(hobby=>{
console.log(`${this.name} love ${hobby}`);
})
}
};
xyz2.printHobbies();//xyz love Coding
//xyz love Sleeping
//xyz love Reading
037 JavaScript的Function关键字,小问题,没看懂,记住就好吧。
var a1 = 3;
function f1() {
var a1 = 2;
return new Function("return a1*a1");
}
console.log(f1()());//9
var a2 = 3;
function f2() {
var a2 = 2;
return function(){return a2*a2};
}
console.log(f2()());//4
038 JavaScript用自执行函数返回一个对象的方式赋值给一个变量,让其成为一个对象。
lab = 8;
var aaa = (function(){
var a = 1;
var bbb = function (){
a++;
console.log(a);
}
function ccc(){
a++;
console.log(a);
}
console.log(lab);
return {
b:bbb,
c:ccc
}
})();
aaa.b(); //2
aaa.c() //3
console.log(aaa);//{b:f,c:f}
lab = 8;
var aaa = (function(){
console.log(lab);
console.log(this.lab);
})();
// 8 8
039 构造函数再回顾
Student = (function (){
var name = '';
Person = function (value){
console.log(value);
}
})()
var p = new Person(555)//555
Student(55)//error Student is not a function
Student = (function (){
var name = '';
var Person = function (value){
console.log(value);
}
})()
var p = new Person(555)//error Persion is not a function
为什么Persion前面加了var以后,Persion就不是构造函数了呢?还有为什么Student不是构造函数呢?
040 字面量式定义对象
var MyAPP = {};
MyAPP.name = {
id:18,
nd:89
};
MyAPP.work = {
num: 123,
sub: {name: "sub_id"},
doing: function(){
console.log(222)
}
}
console.log(MyAPP.name.id)//18
MyAPP.work.doing()//222
(function(window){
var MyAPP = {};
MyAPP.name = {
id:18,
nd:89
};
MyAPP.work = {
num: 123,
sub: {name: "sub_id"},
doing: function(){
console.log(222)
}()
}
//window.MyAPP;
})(window)//222
041 尽管setTimeout函数会把任务一个一个的丢进任务队列,造成for循环中的误会,但是我们可以让丢进去的那个任务立即执行掉,也就是不让任务积攒,让任务队列里面最多只有一个任务。
for(var i=1;i<=5;i++){
setTimeout((function(){
console.log(i);
})(),i*1000);
}
//1 2 3 4 5
042 普通函数的原地立即调用和箭头函数的立即执行的区别,this指针跑丢和没跑丢。
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
function innerFunc(){
console.log(this)
}
innerFunc();
(()=>{
console.log(this)
})()
}
}
obj.show();// 1
obj.outFunc();// window obj
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
function innerFunc(){
console.log(this)
}
innerFunc();
}
}
obj.show();
obj.outFunc();
// 1 window
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
(()=>{
console.log(this)
})();
}
}
obj.show();
obj.outFunc();
// 1 obj
var obj = {
val:1,
show:function(){
console.log(this.val);
},
outFunc:function(){
(function(){
console.log(this)
})();
}
}
obj.show();
obj.outFunc();
// 1 window
043 通过立即执行函数与window互传,把函数绑定到window上去。
(function(window){
var info={
getInfo:function(){
console.log("you are a good boy!");
},
getOff:function(){
console.log("off");
}
}
window.info=info;
})(window);
window.info.getInfo();
window.info.getOff();
(function(window){
var MyApp = {};
MyApp.name = {
id:18,
name:'sheep'
};
MyApp.work = {
num:123,
sub:{name:"sub_id"},
doing:function(){
console.log("you are welcome!");
}
};
window.MyApp = MyApp;
})(window);
window.MyApp.work.doing();
console.log(window.MyApp.work.sub);
或者通过构造函数,在构造函数内部定义一个对象,然后再作为函数的返回值赋值给一个变量,让这个变量变成对象object,通过这个变量来调用函数。
var ggg=(function(){
var info={
getInfo:function(){
console.log("you are a good boy!");
},
getOff:function(){
console.log("off");
}
}
return info;
})();
console.log(typeof ggg)//object
ggg.getInfo();//you are a good boy!
ggg.getOff();//off
直接创建对象,在属性里面附带方法,这种方式也是可以的。
var MyApp = {
name : {
id:18,
name:'sheep'
},
work : {
num:123,
sub:{name:"sub_id"},
doing:function(){
console.log("you are welcome!");
}
}
}
MyApp.work.doing();//you are welcome!
044 对象中普通函数和匿名函数的this指针是根据上下文而确定的,所以diameter函数的this指针是指向shape的,而perimeter函数的this指针是继承父级上下文的,也就是shape的,所以指向window。这个例子中的父级上下文环境解释得很牵强。还有我不知道NaN与undefined之间有什么区别。
radius = 5;
const shape = {
radius:10,
diameter:function(){
return this.radius*2;
},
perimeter:()=>2*Math.PI*this.radius
}
console.log(shape.diameter());//20
console.log(shape.perimeter());//31.415926539
const shape1 = {
radius1: 10,
diameter() {
return this.radius1 * 2;
},
perimeter: () => 2 * Math.PI * this.radius1
}
console.log(shape1.diameter()) // 20
console.log(shape1.perimeter()) // NaN
const shape2 = {
radius2: 10,
diameter() {
return this.radius2 * 2;
},
perimeter: () => {2 * Math.PI * this.radius2}
}
console.log(shape2.diameter()) // 20
console.log(shape2.perimeter()) // undefined
045 复现了别人的一段代码,分析一下。这段代码是用canvas写的一个html单页面小游戏。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple Canvas Game</title>
</head>
<body>
<script src="js/game.js"></script>
</body>
</html>
// Create the canvas var canvas = document.createElement("canvas");//根据字符“canvas”,创建一个canvas对象 var ctx = canvas.getContext("2d");//使用canvas对象的方法,创建一个画笔。 canvas.width = 512; //设置canvas对象的一些属性 canvas.height = 480; document.body.appendChild(canvas);//需要把canvas对象挂载到dom的body节点下,append是附加的方法。 //如果你不是分index.html和game.js两个文件来写,而是把.js文件直接写在.html文件的<script>标签下, //那么可以直接在index.html文件中使用<canvas id="canvas">标签直接创建canvas节点,然后在脚本中 //使用dom的querySelector("canvas")方法来绑定html中的canvas节点。
// Background image var bgReady = false; var bgImage = new Image(); bgImage.onload = function () { bgReady = true; }; bgImage.src = "images/background.png"; // Hero image var heroReady = false; var heroImage = new Image(); heroImage.onload = function () { heroReady = true; }; heroImage.src = "images/hero.png"; // Monster image var monsterReady = false; var monsterImage = new Image(); monsterImage.onload = function () { monsterReady = true; }; monsterImage.src = "images/monster.png"; // Game objects var hero = { speed: 256 // movement in pixels per second }; var monster = {}; var monstersCaught = 0; // Handle keyboard controls var keysDown = {};//这个键盘对象是一个比较难理解的点,和下面的两个事件监听器绑定比较紧密,也是整个程序的核心。 addEventListener("keydown", function (e) { //事件监听函数是写在lib.dom.d.ts里面的,下面详解。参数“keydown”叫类型断言,看该事件监听函数的原型,可以看到。 keysDown[e.keyCode] = true; //参数"keydown"叫类型断言,看事件监听函数和typescript就懂了,keydown是WindowEventMap接口的一个属性,匿名函数是回调用的, }, false); // false是事件监听函数的第三个参数,也是可选参数,typescript里面有可选参数和可选属性的概念,会用一个?修饰,你去看事件监听函数的原型能够看到。 // keyCode是KeyboardEvent接口的一个只读属性,也就是键盘的键码啦。那么匿名函数的参数e就很关键了,它并不是一个普普通通的事件,而是经过"keydown"约束 // 的事件,即键盘按钮按下事件,这个可以在事件监听函数的原型中看到。那么整个函数的功能就是监听键盘,然后把keysDown对象中的键码属性赋值为true。这样,如果 // 如果我按下上箭头按键,那么keysDown对象里面就会有一个键码38,下面我们就可以结合屏幕的刷新频率去模仿人物移动。 addEventListener("keyup", function (e) { //同一个事件监听函数,同一个套路,只不过是删掉keysDown对象里面的键码 delete keysDown[e.keyCode]; }, false); // Reset the game when the player catches a monster var reset = function () { hero.x = canvas.width / 2; hero.y = canvas.height / 2; // Throw the monster somewhere on the screen randomly monster.x = 32 + (Math.random() * (canvas.width - 64)); monster.y = 32 + (Math.random() * (canvas.height - 64)); }; // Update game objects var update = function (modifier) { //这里的数字38、40、37、39实际上是键盘上上下左右键的键码 if (38 in keysDown) { // Player holding up //人物移动是需要考虑屏幕帧率因素的,这个参数modifier就是涉及帧率的 hero.y -= hero.speed * modifier; } if (40 in keysDown) { // Player holding down hero.y += hero.speed * modifier; } if (37 in keysDown) { // Player holding left hero.x -= hero.speed * modifier; } if (39 in keysDown) { // Player holding right hero.x += hero.speed * modifier; } // Are they touching? if ( hero.x <= (monster.x + 32) && monster.x <= (hero.x + 32) && hero.y <= (monster.y + 32) && monster.y <= (hero.y + 32) ) { ++monstersCaught; reset(); } }; // Draw everything var render = function () { if (bgReady) { ctx.drawImage(bgImage, 0, 0); } if (heroReady) { ctx.drawImage(heroImage, hero.x, hero.y); } if (monsterReady) { ctx.drawImage(monsterImage, monster.x, monster.y); } // Score ctx.fillStyle = "rgb(250, 250, 250)"; ctx.font = "24px Helvetica"; ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillText("Goblins caught: " + monstersCaught, 32, 32); }; // The main game loop var main = function () { var now = Date.now(); var delta = now - then; update(delta / 1000); render(); then = now; // Request to do this again ASAP requestAnimationFrame(main); }; // Cross-browser support for requestAnimationFrame var w = window; requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame; // 这里的requestAnimationFrame实际上是4个回调函数,将main函数作为参数进行回调。我比较担心main一旦跑起来,不断的回调自己,会不会造成问题。会内存泄漏吗? // 这4个函数都与屏幕帧率相关。相关函数在lib.dom.d.ts里面有,建议使用idea写html,可以查看底层接口。typescript的:是类型指定。 // Let's play this game! var then = Date.now(); reset(); main();
046 谈谈JavaScript和Typescript之间的关系。TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 是一种给 JavaScript 添加特性的语言扩展。增加的功能包括:
- 类型批注和编译时类型检查
- 类型推断
- 类型擦除
- 接口
- 枚举
- Mixin
- 泛型编程
- 名字空间
- 元祖
- Await
以下功能是从ECMA 2015 反向移植而来:
- 类
- 模块
- lambda 函数的箭头语法
- 可选参数以及默认参数
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
以上就是比较官方的回答了,下面是我自己的见解。JavaScript作为一门饱受诟病又摆脱不了的浏览器语言,用户很多,小毛病也很多,与面向对象、面向过程格格不入。准确的说,它是基于对象的,原型链技术困惑很多人。所以typescript对其进行了一定的封装,让用户更方便的使用。本质上,JavaScript 6 就是对JavaScript 5 的一个封装,JavaScript 5 用起来太难受喽,什么闭包、匿名可以劝退一大批初学者,后来JavaScript 6 对 JavaScript 5 进行了一定的封装,让习惯了面向对象编程的人可以更舒服一些,使它具有了类、模块、可选参数以及默认参数这些java具有的特征。在此之前,JavaScript比较尴尬的就是,它有对象这个概念,却没有类这个概念,变量的类型是弱类型的,又导致函数与基本变量类型、引用变量类型这些概念的界限很模糊,可以使用一个普通变量引用函数类型,然后加上()来进行调用,总之就是很别扭。还好JavaScript 6 对 JavaScript 5 进行了封装,引入了let和const,把var的坑可以填上。本质上JavaScript 6 是通过babel翻译成JavaScript 5 的,所以准确的说,JavaScript 6 是对JavaScript 5 的封装。在JavaScript 6 出来之前,前端大神可以用JavaScript 5 写出很多奇技淫巧的代码,不得不说,这很厉害,但是对新手就没那么友好了。这种做法就跟强行用C语言来写链表一样,属于“造轮子”行为。甚至于有的大神可以用C语言来写PC端窗口游戏,所需要的基础数据结构轮子,都用C语言的结构体和指针来现造。真的很佩服!佩服!佩服!可以说JavaScript 6 已经让JavaScript变得很友好了,以后写代码就严格遵照JavaScript 6 标准就好了,但是你难保不会读到遗留项目里面的JavaScript 5 风格的代码,各种闭包、匿名照样整得你头疼。前面讲到JavaScript 6 给 JavaScript带来了箭头函数,这个其实是模仿java里面的类的静态函数的,java里面类的静态函数是属于类本身的,并不属于实例化后的对象。JavaScript属于一个滥用回调函数的语言,回调函数用多了,this指针就找不到了,因此引入箭头函数以后,让箭头函数绑定到它所属的对象,这样就不会因为多次回调而丢失真正的this指针。然后就是typescript与JavaScript 6 之间的关系了,可以说JavaScript 6 使JavaScript对前端程序员来说变得比较友好了,但是如果想要让JavaScript不只是局限在前端,就需要让其具备更强大的功能,于是typescript对JavaScript 6 进行进一步的封装,让它有了泛型编程、类型断言、接口、名字空间等C++具有的显著特征。C++的显著特征就是泛型编程,这是C++从C语言发展出来时所带来的显著特征。C++的底层就是C语言和汇编,C++的类与对象的底层是C语言的结构体与指针实现的。C++的泛型编程概念让STL库不再局限某一种数据类型,而是通用类型,这里面的功劳就归属泛型编程。typescript这样的引入这些概念,可能是希望JavaScript可以变得应用范围更广大,不再只局限于前端编程。再来说JavaScript 6 中迭代器的概念,其实python中也有迭代器的概念,lua语言中也有迭代器的概念,但是真正说到迭代器的鼻祖,那还得从C++的迭代器说起,迭代器本质上是指针和泛型两种技术做出来的接口,就像可以通过指针去访问变量一样,你可以通过迭代器去访问各种各样的基础数据结构 。其实lua语言也是C语言封装的一个流派,而且lua语言非常的短小精悍,非常值得学习。C语言是非常底层 的语言,C语言的结构体和指针是其通向面向对象的接口,C语言高手可以通过这两样东西写出仿面向对象的代码,但是要想让它去操作数据库、去写多线程、去写纯面向对象的代码,还是太难为它了,光是申请堆栈和释放堆栈就得累死开发者。这种操作就跟用汇编去开发单片机一样,都是噱头,你说你写个流水灯,我能办到,但是让我去写通讯协议,我就佛了。lua是对C语言的一个很好的封装,是一门脚本语言,兼具一些面向对象的操作,可以访问数据库等等。如果想要学习大数据spark,那么就必须学习sacle语言,巧了,scale是对java的封装,java是对C语言的封装。^_^ Typescript同时保留了JavaScript中的var和let,其实我觉得这样并不好,用var定义一个变量,然后用冒号:进行类型指定,不如让let来干这件事。
047 对事件监听函数的详细解释。在045那个例子中碰到了一个事件监听函数addEventListener,它实际是一个回调函数,类似迭代器,下面是对它的详细解释。我决定从C语言的函数指针和回调函数讲起。
函数指针是指向函数的指针变量,比如int (*func_ptr)(int,int)、int sum(int x,int y),通过函数指针,C语言可以实现各种强大的功能与设计方法。而回调函数是函数指针最常见的用途,也是C语言面试中的必考知识点和难点。函数名可以当做一个常量来看待,它保存了函数的内存地址,函数的内存地址存储了函数开始执行的位置,使用指针保存函数的地址,即让指针指向这个函数, func_ptr = sum ,指向函数的指针被称作是函数指针。通过函数指针,可以灵活的调用各种形式相同但功能不同的函数。比如
#include <stdio.h>
int a = 5;
int b = 8;
int (*func_ptr)(int,int);
int sum(int x,int y)
{
return x+y;
}
int difference(int x,int y)
{
return x-y;
}
int product(int x,int y)
{
return x*y;
}
int main()
{
func_ptr = sum;
printf("%d\n",func_ptr(a,b));//13
func_ptr = difference;
printf("%d\n",func_ptr(a,b));//-3
func_ptr = product;
printf("%d\n",func_ptr(a,b));//40
}
这样做,大大的增加了代码的灵活程度。比如定义三个函数,它们具有相同的形式参数和返回值,分别计算两个数相加、相减和相乘,在main函数中定义a、b两个变量,再定义一个有两个整数参数且返回值是整数的函数指针func_ptr,将func_ptr分别指向函数sum、difference、product,就可以用函数指针来调用这三个函数了。
和其它变量一样,函数指针也可以作为函数参数传递给函数,比如
int compute_func(int (*func_ptr)(int,int),int x,int y){
return func_ptr(x,y);
}
这样函数内部就可以根据函数指针所指向函数的不同而调用不同的函数了。在这个函数中,通过该函数指针func_ptr调用的函数sum、difference、product被称为回调函数。这种开发方式的用途非常广泛。具体来说,在回调函数的应用场景中,会出现两个角色,分别是某功能函数的开发者与该功能函数的使用者。一般开发者会开发一个较为通用的功能函数。使用该函数时,使用者需要给函数传入一个函数指针func_ptr,这个函数指针指向的函数是使用者开发的,在功能函数运行时,通过传入的函数指针调用使用者开发的函数的过程,就是一个回调的过程。而使用者开发的这个函数就被称为回调函数了。通用的功能函数compute_func有三个参数,函数指针func_ptr和整数a、b,其中func_ptr包括两个整型的参数和整型的返回值,与sum、difference、product形式相同。在函数compute_func中直接返回func_ptr的调用结果,也就是对于使用者来说,传入哪个函数,compute_func就通过func_ptr调用哪个函数。比如
printf("%d\n",compute_func(sum,a,b));//13
printf("%d\n",compute_func(difference,a,b));//-3
printf("%d\n",compute_func(product,a,b));//40
这里compute_func就是某功能函数,开发者指的是开发compute_func的人,而sum、difference、product这三个函数就是使用者开发的回调函数。一般来说,开发者和使用者不是同一个人,并且开发者开发的功能函数都会比较复杂,另外无论回调函数执行什么功能,只要使用者传入的回调函数与函数指针的形式一样就可以了。
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
这样就很像了,但是呢JavaScript的功能函数不可能写得跟C语言的完全相同,只能是形似,它们有相同的地方。再看它的原型,传入的回调函数的this指针为window,事件参数是windowEventMap里面的k,也就是说这个回调函数的参数是事件监听函数的第一个参数,即将keydown作为参数传入到匿名函数中。e就是keydown,这里就是
keysDown[keydown.keyCode] = true,也就是说keysDown对象会添加一个keyCode相对应的keydown属性。
declare function addEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
048 今天来谈一个学习JavaScript中碰到的问题,JavaScript有很多内置函数, 它们的声明是写在lib.dom.d.ts或者lib.es5.d.ts里面的,我们需要经常去看。就好像学习C语言的时候,也需要经常去看C语言内置函数的声明,这些声明文件是非常重要的,学习编程到了这个阶段就需要能够读懂声明文件和函数定义文件。VScode写JavaScript好像不支持右击查看声明文件。VC6.0写C语言是可以看到C语言的声明文件的,这些声明文件一般写法比较巧妙,如果能够读懂,那么就是编程能力的一大提升。
049 JavaScript中foreach函数的使用,本质上是一个迭代器。
// 将会显示两个 logs。 一个是 "0 = zero" 另一个是 "1 = one"
myMap.forEach(function(value, key) {
console.log(key + " = " + value);// 0 = zero 1 = one
console.log(this);//map
}, myMap)
myMap.forEach((value, key) => {
console.log(key + " = " + value);// 0 = zero 1 = one
console.log(this)//window
},myMap);
myMap.forEach(function(value, key) {
console.log(key + " = " + value);// 0 = zero 1 = one
console.log(this);//window
})
myMap.forEach((value, key) => {
console.log(key + " = " + value);// 0 = zero 1 = one
console.log(this)//window
});
通过分析可以指定foreach迭代器里面指定参数"myMap"的时候,你往foreach里面塞一个匿名函数,这个匿名函数的this指针是指向这个myMap的,当你忽略了参数"myMap"时,匿名函数的this指针则指向window;而箭头函数完全不吃这一套,它的this指针不受迭代器foreach的影响,始终指向window。也就是说这个箭头函数的this指针是继承自window的,不会受迭代器的影响。
在学习vue的过程中,会碰到驼峰命名法和短横线命名法。vue组件名和prop都可以用驼峰命名和短横线命名,但html里面只能使用短横线。(js中的驼峰命名会自动转换成短横线命名。)比如HelloWorld组件,在App.vue中使用html方式使用时,就变成了<hello-world></hello-world>,当然了,现在的vue版本中又改过了了,它的html中同时支持驼峰命名和短横线命名,也就是说注册一个HelloWorld组件,那么在html中可以使用<HelloWorld>和<hello-world>两种方式来调用。