Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS手写系列 #10

Open
zhjm123 opened this issue Dec 7, 2020 · 0 comments
Open

JS手写系列 #10

zhjm123 opened this issue Dec 7, 2020 · 0 comments

Comments

@zhjm123
Copy link
Owner

zhjm123 commented Dec 7, 2020

1.彻底弄懂jsonp原理及实现方法

一、 同源策略

同源策略,它是由Netscape提出的一个著名的安全策略。
现在所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。
当一个浏览器的两个tab页中分别打开百度和谷歌的页面
当一个百度浏览器执行一个脚本的时候会检查这个脚本是属于哪个页面的
即检查是否同源,只有和百度同源的脚本才会被执行。

二、JSON和JSONP

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。
JSONP是JSON with Padding的略称。它是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。

三:JSONP的实现模式--CallBack

程序A中sample的部分代码:

<script type="text/javascript">
//回调函数
function callback(data) {
    alert(data.message);
}
</script>
<script type="text/javascript" src="http://localhost:20002/test.js"></script>

程序B中test.js的代码:

//调用callback函数,并以json数据形式作为阐述传递,完成回调
callback({message:"success"});

这其实就是JSONP的简单实现模式,或者说是JSONP的原型:创建一个回调函数,然后在远程服务上调用这个函数并且将JSON 数据形式作为参数传递,完成回调。

最后优化总结一下代码:我们希望这个script标签能够动态的调用,而不是像上面因为固定在html里面所以没等页面显示就执行了,很不灵活。我们可以通过javascript动态的创建script标签,这样我们就可以灵活调用远程服务了。

<script type="text/javascript">
   //添加<script>标签的方法
   function addScriptTag(src){
       var script = document.createElement('script');
       script.setAttribute("type","text/javascript");
       script.src = src;
       document.body.appendChild(script);
   }
   
   window.onload = function(){
       //搜索apple,将自定义的回调函数名result传入callback参数中
       addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
       
   }
   //自定义的回调函数result
   function result(data) {
       //我们就简单的获取apple搜索结果的第一条记录中url数据
       alert(data.responseData.results[0].unescapedUrl);
   }
</script>

2.深拷贝

目前实现深拷贝的方法不多,主要是两种:
1.利用 JSON 对象中的 parse 和 stringify
2.利用递归来实现每一层都重新创建对象并赋值

1.JSON.stringify/parse的方法
undefined、function、symbol 会在转换过程中被忽略。。。

2.递归的方法
递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:

function deepClone(source){
        const targetObj =   source.constructor === Array ? [] : {}
        for ( let keys  in  source){
            if(source.hasOwnProperty(keys)){
                if(source[keys]&&typeof  source[keys] ==='object'){
                    targetObj[keys]  = source[keys].constructor === Array ? [] : {};
                    targetObj[keys] = deepClone( source[keys] )
                }else {
                    targetObj[keys] = source[keys]
                }
            }
        }
        return targetObj
   }

三:深度解析 call 和 apply 原理、使用场景及实现

call() 和 apply()

call() 和 apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组

使用场景

1、合并两个数组

var vegetables = ['parsnip', 'potato'];
var moreVegs = ['celery', 'beetroot'];

Array.prototype.push.apply(vegetables,moreVegs)

vegetables
// ['parsnip', 'potato', 'celery', 'beetroot']

2、获取数组中的最大值和最小值

var numbers = [5, 458 , 120 , -215 ]; 
Math.max.apply(Math, numbers);   //458    
Math.max.call(Math, 5, 458 , 120 , -215); //458

// ES6
Math.max.call(Math, ...numbers); // 458

为什么要这么用呢,因为数组 numbers 本身没有 max 方法,但是 Math 有呀,所以这里就是借助 call / apply 使用 Math.max 方法。

3、利用它判断类型

console.log(Object.prototype.toString.call("jerry"));//[object String]
console.log(Object.prototype.toString.call(12));//[object Number]
console.log(Object.prototype.toString.call(true));//[object Boolean]
console.log(Object.prototype.toString.call(undefined));//[object Undefined]
console.log(Object.prototype.toString.call(null));//[object Null]
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object]
console.log(Object.prototype.toString.call(function(){}));//[object Function]
console.log(Object.prototype.toString.call([]));//[object Array]
console.log(Object.prototype.toString.call(new Date));//[object Date]
console.log(Object.prototype.toString.call(/\d/));//[object RegExp]
function Person(){};
console.log(Object.prototype.toString.call(new Person));//[object Object]

4、类数组对象(Array-like Object)使用数组方法

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

手写call

call的模拟实现

模拟实现第一步
如果在调用call()的时候把函数 bar()添加到foo()对象中,即如下

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
};
foo.bar(); // 1

这个改动就可以实现:改变了this的指向并且执行了函数bar。

但是这样写是有副作用的,即给foo额外添加了一个属性,怎么解决呢?

解决方法很简单,用 delete 删掉就好了。

所以只要实现下面3步就可以模拟实现了。

1、将函数设置为对象的属性:foo.fn = bar
2、执行函数:foo.fn()
3、删除函数:delete foo.fn
代码实现如下:

// 第一版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this; 		// foo.fn = bar
    context.fn();			// foo.fn()
    delete context.fn;		// delete foo.fn
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call2(foo); // 1

模拟实现第二步
第一版有一个问题,那就是函数 bar 不能接收参数,所以我们可以从 arguments中获取参数,取出第二个到最后一个参数放到数组中,为什么要抛弃第一个参数呢,因为第一个参数是 this。

类数组对象转成数组的方法上面已经介绍过了,但是这边使用ES3的方案来做。

var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
}

参数数组搞定了,接下来要做的就是执行函数 context.fn()。

context.fn( args.join(',') ); // 这样不行

所以说第二个版本就实现了,代码如下:

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call2(foo, 'kevin', 18); 
// kevin
// 18
// 1

模拟实现第三步

还有2个细节需要注意:

1、this 参数可以传 null 或者 undefined,此时 this 指向 window
2、this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
3、函数是可以有返回值的

实现上面的三点很简单,代码如下

// 第三版
Function.prototype.call2 = function (context) {
    context = context ? Object(context) : window; // 实现细节 1 和 2
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result; // 实现细节 2
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

function foo() {
    console.log(this);
}

bar.call2(null); // 2
foo.call2(123); // Number {123, fn: ƒ}

bar.call2(obj, 'kevin', 18);
// 1
// {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

参考:https://github.com/yygmind/blog/issues/22

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant